├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── DESIGN.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── inspec-iggy.gemspec ├── lib ├── inspec-iggy.rb └── inspec-iggy │ ├── cloudformation │ ├── cli_command.rb │ └── generate.rb │ ├── file_helper.rb │ ├── iggy_cli_command.rb │ ├── inspec_helper.rb │ ├── platforms │ ├── aws_helper.rb │ ├── azure_helper.rb │ └── gcp_helper.rb │ ├── plugin.rb │ ├── profile_helper.rb │ ├── terraform │ ├── cli_command.rb │ ├── generate.rb │ └── negative.rb │ └── version.rb └── test ├── fixtures ├── cloudformation │ ├── aws-4.5.4.json │ ├── bad.json │ └── bjc-demo-aws-5.1.4.json └── terraform │ ├── configs │ ├── main.tf │ ├── outputs.tf │ └── variables.tf │ └── tfstates │ ├── aws-terraform-elb-example.tfstate │ ├── aws-terraform-two-tier-example.tfstate │ ├── azure-terraform.tfstate │ └── gcp-terraform.tfstate ├── functional ├── cli_help_spec.rb ├── cloudformation_spec.rb └── terraform_spec.rb ├── helper.rb ├── inspec ├── README.md ├── controls │ ├── inspec.rb │ ├── inspec_cloudformation.rb │ ├── inspec_iggy.rb │ ├── inspec_terraform.rb │ └── inspec_terraform_aws.rb └── inspec.yml ├── integration └── resource_spec.rb └── unit ├── cloudformation_cli_args_spec.rb ├── plugin_def_spec.rb ├── terraform_cli_args_spec.rb └── version_spec.rb /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Environment** 14 | Which operating system and versions of InSpec, Terraform, and InSpec-Iggy plugin were you using? 15 | 16 | **To Reproduce** 17 | Please provide the commands used to reproduce the behavior. If controls are being rendered that do not work, please post the generated controls.rb. If possible please share sanitized source files. 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. Suggestions related to the implementation are appreciated. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.html 3 | .bundle 4 | Gemfile.lock 5 | pkg/* 6 | .idea/ 7 | iggy-test-profile 8 | .bundler/ 9 | test/inspec/inspec.lock 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'test/kitchen/**/*' 4 | - 'test/integration/**/controls/**/*.rb' 5 | - 'test/fixtures/profiles/**/*.rb' 6 | - 'test/fixtures/config_dirs/**/*.rb' 7 | - 'lib/plugins/inspec-init/templates/plugins/inspec-plugin-template/**/*' 8 | - 'examples/**/controls/*.rb' 9 | - 'vendor/bundle/**/*' 10 | Layout/AlignArguments: 11 | EnforcedStyle: with_first_argument 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.4 6 | - 2.5 7 | - 2.6 8 | script: 9 | - bundle exec rake lint 10 | - bundle exec rake test 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | This is the current, previous and future development milestones and contains the features backlog. 2 | 3 | # 0.1.0 # 4 | * Initial prototype supporting a terraform.tfstate from the AWS provider and tagged profiles 5 | * Produces a dynamic set of AWS generated controls 6 | 7 | # 0.2.0 # 8 | * switched to Apache v2 license 9 | * switched to to_ruby (Christoph Hartmann) 10 | * rename to inspec-iggy 11 | * switched to InSpec plugin 12 | * moved to https://github.com/inspec/inspec-iggy 13 | * published to Rubygems 14 | 15 | # 0.3.0 # 16 | * CloudFormation support through the stack-name entry 17 | * Wrap control in a full profile for upload 18 | * document Linux Omnibus installer usage 19 | * More profile options to fill out the inspec.yml from the CLI 20 | * .rubocop.yml synced to InSpec v2.2.79 and Rubocop 0.55 21 | * Switch to Inspec::BaseCLI for the helper methods 22 | * use new plugin include path (for old v1 plugins) @chris-rock 23 | * allowing for multiple modules to be included in generate output @devoptimist 24 | 25 | # 0.4.0 # 26 | * Primarily @clintoncwolfe, refactoring and modifying for Plugin API 27 | * Overhaul to match InSpec Plugin API2/InSpec v3.0 28 | * Place code under InspecPlugins::Iggy namespace 29 | * Re-Organize tests 30 | * Add tests for testing plugin interface 31 | * Add tests for testing user functionality 32 | * Expand Rakefile 33 | 34 | # 0.5.0 35 | * provide DESIGN.md explaining the organization of the code 36 | * disabled the `inspec terraform extract` subcommand until a more sustainable solution is determined 37 | * moved back to https://github.com/mattray/inspec-iggy as a community plugin 38 | * Sync and upgrade InSpec's .rubocop.yml and associated code cleanups 39 | * rename lib/inspec-iggy/profile.rb to profile_helper.rb 40 | * refactor out JSON parsing into file_helper.rb 41 | * switch from 'eq' to 'cmp' comparators https://github.com/mattray/inspec-iggy/issues/23 42 | * enable minimal Azure support. This needs to be refactored. 43 | * add support for remote .tfstate and .cfn files via Iggy::FileHelper.fetch https://github.com/mattray/inspec-iggy/issues/3 44 | 45 | # 0.6.0 46 | * InSpec 4.0 support added 47 | * enable AWS, Azure, and GCP platform and resource pack support 48 | * `inspec terraform negative` was added, providing negative coverage testing 49 | * unit tests were broken by updates in InSpec and fixed. Functional and integration tests were disabled for now. 50 | * switch to Chefstyle like InSpec and Chefstyle the generated controls 51 | 52 | # 0.7.0 (The SysAdvent demo Release) 53 | * added 'inspec iggy' subcommand for displaying help and version 54 | * Terraform 0.12 support 55 | * Restored initial AWS support, minimal testing 56 | * aws_ec2_instance, aws_elb, aws_security_group, aws_subnet, aws_vpc 57 | * [Terraform AWS Provider Two Tier demo](https://github.com/terraform-providers/terraform-provider-aws/tree/master/examples/two-tier) 58 | 59 | # 0.8.0 (Terraform AWS demos release) 60 | * make platform and resourcepack required 61 | * aws_alb, aws_cloudformation_stack, aws_cloudtrail_trail, aws_route_table added without testing, expect issues 62 | * [Terraform AWS Provider ELB demo](https://github.com/terraform-providers/terraform-provider-aws/tree/master/examples/) 63 | * create new InSpec tests to validate the generated reports to look for regressions as we change out the property mapping. It's too manual and fragile. 64 | 65 | # 0.8.1 66 | * look into refactoring discovery of resources and properties instead of hard-coded technique 67 | * clean up deprecation warnings by using the Inspec::Object classes from the inspec-objects rubygem 68 | 69 | # NEXT 70 | * Restore and re-test AWS, Azure, GCP from resource packs using their Terraform plans 71 | * Verify CloudFormation support 72 | * Implement ARM templates 73 | * document inspec with a reporter to push the reports into Automate 74 | * document uploading profiles to Automate and creating scan jobs via API 75 | * document/specify inspec-aws https://github.com/inspec/inspec-aws/releases 76 | * add negative testing for CloudFormation 77 | 78 | # BACKLOG # 79 | * CloudFormation can be JSON or YAML 80 | * allow disabling of individual negative tests from CLI? 81 | * additional attributes (ie. vpc_id) passed via inputs? 82 | * allow passing alternate source of depends profiles 83 | * document Windows Omnibus installer usage 84 | * Habitat packaging 85 | * Terraform 86 | * More Terraform back-ends https://www.terraform.io/docs/backends/types/index.html 87 | * do we want to generate inspec coverage for the tfplan? 88 | * restore extract functionality 89 | * create a Terraform Provisioner for attaching InSpec profiles to a resource 90 | * Tie tagged compliance profiles back to machines and non-machines where applicable (ie. AWS Hong Kong) 91 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | This document attempts to explain the organization of the InSpec-Iggy code and how to extend it as necessary. Because Iggy is an InSpec plugin, it tries to follow InSpec closely with regards to versions, style, and tooling. Links to the source code are given because there may be additional documentation within the files. 4 | 5 | # Files 6 | 7 | * [.rubocop.yml](#rubocop) 8 | * [CHANGELOG.md](#changelog) 9 | * [Gemfile](#gemfile) 10 | * [inspec-iggy.gemspec](#gemspec) 11 | * [lib/inspec-iggy.rb](#iggy) 12 | * [lib/inspec-iggy/plugin.rb](#plugin) 13 | * [lib/inspec-iggy/file_helper.rb](#ile_helper) 14 | * [lib/inspec-iggy/inspec_helper.rb](#inspec_helper) 15 | * [lib/inspec-iggy/profile_helper.rb](#profile_helper) 16 | * [lib/inspec-iggy/version.rb](#version) 17 | * [lib/inspec-iggy/platforms/aws_helper.rb](#aws_helper) 18 | * [lib/inspec-iggy/platforms/azure_helper.rb](#azure_helper) 19 | * [lib/inspec-iggy/platforms/gcp_helper.rb](#gcp_helper) 20 | * [lib/inspec-iggy/terraform/cli_command.rb](#tf_cli) 21 | * [lib/inspec-iggy/terraform/generate.rb](#tf_generate) 22 | * [lib/inspec-iggy/terraform/negative.rb](#tf_negative) 23 | * [lib/inspec-iggy/cloudformation/cli_command.rb](#cfn_cli) 24 | * [lib/inspec-iggy/cloudformation/generate.rb](#cfn_generate) 25 | 26 | ## [.rubocop.yml](.rubocop.yml) 27 | 28 | Tracks against InSpec's settings for code style, currently using [Chefstyle 0.13.0](https://github.com/chef/chefstyle). 29 | 30 | ## [CHANGELOG.md](CHANGELOG.md) 31 | 32 | Has the rough feature set by each release but also contains the BACKLOG for the project, ideas considered but not yet implemented. 33 | 34 | ## [Gemfile](Gemfile) 35 | 36 | The source of the gems and additional gemsets for use with Bundler (ie. `test`). 37 | 38 | ## [inspec-iggy.gemspec](inspec-iggy.gemspec) 39 | 40 | This is where metadata for the Gem goes. We have also pinned the version of InSpec to between 2.3 and less than 5 to prevent breaking changes. 41 | 42 | ## [lib/inspec-iggy.rb](lib/inspec-iggy.rb) 43 | 44 | This is the "entry point" for InSpec to load if it thinks the plugin is installed. The *only* thing this file should do is setup the load path, then load the plugin definition file. 45 | 46 | ## [lib/inspec-iggy/plugin.rb](lib/inspec-iggy/plugin.rb) 47 | 48 | Plugin Definition file. The purpose of this file is to declare to InSpec what plugin_types (capabilities) are included in this plugin, and provide hooks that will load them as needed. It is important that this file load successfully and *quickly*. The plugin's functionality may never be used on this InSpec run; so we keep things fast and light by only loading heavy things when they are needed. 49 | 50 | The entry points for the `cli_command`s for `:terraform` and `:cloudformation` are here. If you were to add another format this is the place to declare that. 51 | 52 | # Helpers 53 | 54 | ## [lib/inspec-iggy/file_helper.rb](lib/inspec-iggy/file_helper.rb) 55 | 56 | Helper class that parses JSON input files and handles errors. 57 | 58 | ## [lib/inspec-iggy/inspec_helper.rb](lib/inspec-iggy/inspec_helper.rb) 59 | 60 | Constants and helper methods for working with InSpec. 61 | 62 | ### Constants 63 | 64 | * `TRANSLATED_RESOURCES`: Resources that do not map cleanly are provided by the `TRANSLATED_RESOURCES` hash. There are very few mismatches because both tools use the SDKs provided by the same vendors. 65 | * `ADDITIONAL_COMMON_PROPERTIES`: Because InSpec properties are often dynamically generated, it is hard to determine their existence without instantiating them. Because of this, we maintain a manual list of properties to check for. 66 | 67 | ### Helper Methods 68 | 69 | * `available_resources`: The list of currently available InSpec Resources. 70 | * `load_resource_pack(resource_path)`: Adds a resource pack's Resources to the `available_resources`. 71 | * `available_resource_qualifiers(platform)`: The available qualifers for the Describe block within the Controls generated for a particular resource. 72 | * `available_resource_iterators(platform)`: The iterators available for resources, also provides the qualifiers for those iterators. Used for iterating over negative coverage. 73 | * `translated_resource_property(platform, resource, property)`: Resource properties that do not map cleanly are looked up from the associated platform, returning the property whether or not it is translated. This is currently used for mapping properties like `name` to `group_name` for example. 74 | * `tf_controls`: provides the content for the controls file for Terraform subcommand. 75 | * `cfn_controls`: provides the AWS API calls to dynamically check the passed CloudFormation stack and provide the content for the controls file. 76 | 77 | ## [lib/inspec-iggy/profile_helper.rb](lib/inspec-iggy/profile_helper.rb) 78 | 79 | Helper class to render a full InSpec profile with a `README.md`, `inspec.yml`, and the generated `controls/generated.rb` populated from the parsed input file and the CLI options provided. 80 | 81 | ## [lib/inspec-iggy/version.rb](lib/inspec-iggy/version.rb) 82 | 83 | Tracks the version of InSpec-Iggy. 84 | 85 | # Platform Helpers 86 | 87 | ## [lib/inspec-iggy/platforms/aws_helper.rb](lib/inspec-iggy/platforms/aws_helper.rb) 88 | ## [lib/inspec-iggy/platforms/azure_helper.rb](lib/inspec-iggy/platforms/azure_helper.rb) 89 | ## [lib/inspec-iggy/platforms/gcp_helper.rb](lib/inspec-iggy/platforms/gcp_helper.rb) 90 | 91 | The platform helpers provide constants used by the [inspec_helper.rb](#inspec_helper) for translating and filtering resources and their iterators, qualifiers, and properties. They also provide methods used by the [profile_helper.rb]((#profile_helper)) to render the platform-specific instructions for the generated InSpec profiles. 92 | 93 | # Terraform 94 | 95 | ## [lib/inspec-iggy/terraform/cli_command.rb](lib/inspec-iggy/terraform/cli_command.rb) 96 | 97 | The `inspec terraform` CLI command and options. Given this is a Thor CLI, the `desc` and `subcommand_desc` provide help at the CLI. The `class_option`s hash is used to define documentation and settings for allowed subcommand options. Each method (`generate` and `negative`) is turned into further subcommands (ie. `inspec terraform generate`) and there are currently no differences in options between them. 98 | 99 | Within the `generate` method the following block: 100 | 101 | generated_controls = InspecPlugins::Iggy::Terraform::Generate.parse_generate(options[:tfstate], resource_path, platform) 102 | 103 | calls into the [Terraform generate_parse_generate](#tf_generate) which returns the InSpec controls found by mapping Terraform resources to InSpec resources given a platform and the path to its resources. 104 | 105 | printable_controls = InspecPlugins::Iggy::InspecHelper.tf_controls(options[:title], generated_controls, platform) 106 | 107 | calls into the [inspec helper](#inspec_helper) to produce the InSpec controls to include within the profile, filtering on the platform. 108 | 109 | InspecPlugins::Iggy::ProfileHelper.render_profile(ui, options, options[:tfstate], printable_controls, platform) 110 | 111 | calls into the [profile renderer](#profile_helper). 112 | 113 | The `inspec terraform negative` command uses the same options as the `generate` command and follows the same pattern of parsing the controls, converting them to a printable format, and printing the output as an InSpec profile. 114 | 115 | ## [lib/inspec-iggy/terraform/generate.rb](lib/inspec-iggy/terraform/generate.rb) 116 | 117 | This class parses the passed Terraform .tfstate files. The `parse_generate` method is the standard interface for parsing, it calls into the private `InspecPlugins::Iggy::FileHelper.parse_json` method which reads the actual JSON. 118 | 119 | The `parse_resources` method then parses the Terraform JSON, iterating over the `modules` array of `resources` which are then mapped to the appropriate InSpec Resources. The `parse_resources` method calls into the [`InSpecHelper`](#inspec_helper) to `load_resource_pack` to load the additional InSpec Resources provided by the respective platform's resource pack. The [TRANSLATED_RESOURCES](#inspec_helper) provide a mapping of Terraform resources that do not match the InSpec equivalent. The resources that map from Terraform to InSpec are returned. 120 | 121 | The parsed resources are then passed to `parse_controls` which generates InSpec Controls and tests for the matched resources. The generated InSpec controls are returned. 122 | 123 | ## [lib/inspec-iggy/terraform/negative.rb](lib/inspec-iggy/terraform/negative.rb) 124 | 125 | The `Negative` class reuses the `InspecPlugins::Iggy::FileHelper.parse_json` and `InspecPlugins::Iggy::Terraform::Generate.parse_resources` to parse the JSON and find the matched resources respectively. 126 | 127 | Negative controls are generated by finding the platform resources that are not represented by Terraform (`parse_unmatched_resources`) and those that are managed with Terraform (`parse_matched_resources`). 128 | 129 | * `parse_unmatched_resources` iterates over all of the of `InspecPlugins::Iggy::InspecHelper.available_resource_iterators` that are not present in the matched resources. It then creates Controls that test that they `should_not exist` since they are not managed by Terraform. 130 | * `parse_matched_resources` iterates over each matched resource and removes them from the entire set of that resource. If there are any remaining resources they are not managed by Terraform, so we test that they `should_not exist`. Because we are embedding iterators in our Control, we have to render this control by hand rather than use InSpec's Control object. 131 | 132 | # CloudFormation 133 | 134 | ## [lib/inspec-iggy/cloudformation/cli_command.rb](lib/inspec-iggy/cloudformation/cli_command.rb) 135 | 136 | The CFN `cli_command.rb` is similar to the [terraform/cli_command.rb](#tf_cli). It requires a `:stack` as an option, because it will dynamically generate the InSpec profile from the launched CloudFormation stack in conjunction with the template. 137 | 138 | ## [lib/inspec-iggy/cloudformation/generate.rb](lib/inspec-iggy/cloudformation/generate.rb) 139 | 140 | The CFN parser is very similar to the [terraform/generate.rb](#tf_generate), parsing a JSON template file and iterating over the 'Resources'. 141 | 142 | # Platform Support 143 | 144 | ## Terraform 145 | 146 | For InSpec-Iggy to work, you must have both Terraform and InSpec support for your platform. This is because it maps Terraform resources to InSpec resources. You will need to provide the path to the proper InSpec resource pack providing your platform's resources. If there's not an InSpec plugin for the platform, there won't be any resources generated. 147 | 148 | If you have working InSpec and Terraform support, you will want to run with 149 | 150 | inspec terraform generate -t terraform.tfstate --platform PLATFORM --resourcepath ~/ws/inspec-PLATFORM --name DEBUG --debug 151 | 152 | and look through the debugging messages to see what is being `SKIPPED`, `TRANSLATED` or `MATCHED`. You may want to drop a `pry` debugging breakpoint within the [Terraform generate](#tf_generate) `parse_resources` method to see what is in the JSON versus what InSpec resources. 153 | 154 | If you are not getting `MATCHED` `resource_type` resources and all `SKIPPED`, they are most likely not in the `InspecPlugins::Iggy::InspecHelper::RESOURCES`. The `TRANSLATED_RESOURCES` within the [inspec_helper.rb](#inspec_helper) may need to be updated to map `resource_type`s to what is in InSpec. 155 | 156 | At this point there are not mappings for InSpec properties to Terraform attributes. If this is an issue you may need to update the hash of resources and the attribute mappings in the [inspec_helper.rb](#inspec_helper). 157 | 158 | ### New Platforms 159 | 160 | AWS, Azure, and GCP are currently supported in the [lib/inspec-iggy/platforms/]. If you wish to add another platform start with those helpers and provide the same constants and methods, assuming you have Terraform and InSpec support. 161 | 162 | ## Alternate Formats 163 | 164 | If you want to add support for another format (ie. ARM templates or something similar), follow the examples of the [Terraform](#tf) and [CloudFormation](#cfn) support. You will start by adding a new `cli_command` to the [lib/inspec-iggy/plugin.rb](#plugin). You will need a `cli_command.rb` and `parser.rb` implementing the appropriate classes and methods. 165 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | source "http://rubygems.org" 3 | 4 | gemspec 5 | 6 | # follows InSpec's versions 7 | group :test do 8 | gem "inspec-bin", ">=3", "<5" 9 | gem "chefstyle", "~> 0.13.0" 10 | gem "minitest", "~> 5.5" 11 | gem "rake", ">= 10" 12 | gem "m" 13 | gem "pry", "~> 0.10" 14 | gem "pry-byebug" 15 | end 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NO LONGER UNDER DEVELOPMENT 2 | 3 | InSpec-Iggy has not been under active development for awhile and I no longer work at Chef, so I'm archiving the project unless someone wants to continue development. 4 | 5 | # Description # 6 | 7 | [![Build Status Master](https://travis-ci.org/mattray/inspec-iggy.svg?branch=master)](https://travis-ci.org/mattray/inspec-iggy) 8 | 9 | InSpec-Iggy (InSpec Generate -> "IG" -> "Iggy") is an [InSpec](https://inspec.io) plugin for generating compliance controls and profiles from [Terraform](https://terraform.io) `tfstate` files and [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates. Iggy generates InSpec controls by mapping Terraform and CloudFormation resources to InSpec resources and exports a profile that may be used from the `inspec` CLI and report to [Chef Automate](https://automate.chef.io/). 10 | 11 | inspec terraform generate -n myprofile --platform aws --resourcepath /tmp/inspec-aws 12 | inspec exec myprofile -t aws://us-west-2 13 | 14 | Iggy was originally a stand-alone CLI inspired by Christoph Hartmann's [inspec-verify-provision](https://github.com/chris-rock/inspec-verify-provision) and the blog post on testing [InSpec for provisioning testing: Verify Terraform setups with InSpec](http://lollyrock.com/articles/inspec-terraform/). 15 | 16 | The [CHANGELOG.md](https://github.com/mattray/iggy/blob/master/CHANGELOG.md) covers current, previous and future development milestones and contains the features backlog. 17 | 18 | 1. [Requirements](#requirements) 19 | 2. [Support](#support) 20 | 3. [Installation](#installation) 21 | 4. [InSpec Terraform Generate](#itg) 22 | 5. [InSpec Terraform Negative](#itn) 23 | 6. [InSpec Cloudformation Generate](#icg) 24 | 7. [Development and Testing](#development) 25 | 26 | # Support 27 | 28 | InSpec-Iggy is a community-driven plugin that is not officially supported by Chef. We welcome patches, suggestions, and issues. 29 | 30 | # Requirements 31 | 32 | Iggy generates compliance profiles for InSpec 3 and later, requiring external resource packs for the AWS, Azure, and GCP resources. Because resources are continuing to be added to InSpec, you may want the latest version to support as much resource coverage as possible. 33 | 34 | Written and tested with Ruby 2.6 and InSpec 4. 35 | 36 | # Installation 37 | 38 | `inspec-iggy` is a plugin for InSpec. InSpec 3 or later is required. To install, use: 39 | 40 | $ inspec plugin install inspec-iggy 41 | 42 | You will need to download the [inspec-aws](https://github.com/inspec/inspec-aws)|[inspec-azure](https://github.com/inspec/inspec-azure)|[inspec-gcp](https://github.com/inspec/inspec-gcp) resources packs as necessary and place them in your path for loading via `--resourcepath`. 43 | 44 | # InSpec Terraform Generate 45 | 46 | inspec terraform generate --tfstate terraform.tfstate --name myprofile --platform aws --resourcepath /tmp/inspec-aws 47 | 48 | Iggy dynamically pulls the available Cloud resources from InSpec and attempts to map them to Terraform resources, producing an InSpec profile. ```inspec terraform generate --help``` will show all available options. 49 | 50 | ## Usage 51 | 52 | inspec terraform generate [options] -n, --name=NAME 53 | 54 | -n, --name=NAME Name of profile to be generated (required) 55 | -t, [--tfstate=TFSTATE] Specify path to the input terraform.tfstate (default: .) 56 | --platform=gcp|aws|azure Cloud provider name 57 | --resourcepath=PATH Location of inspec-gcp|inspec-aws|inspec-azure resources 58 | [--copyright=COPYRIGHT] Name of the copyright holder (default: The Authors) 59 | [--email=EMAIL] Email address of the author (default: you@example.com) 60 | [--license=LICENSE] License for the profile (default: Apache-2.0) 61 | [--maintainer=MAINTAINER] Name of the copyright holder (default: The Authors) 62 | [--summary=SUMMARY] One line summary for the profile (default: An InSpec Compliance Profile) 63 | [--title=TITLE] Human-readable name for the profile (default: InSpec Profile) 64 | [--version=VERSION] Specify the profile version (default: 0.1.0) 65 | [--overwrite], [--no-overwrite] Overwrites existing profile directory 66 | [--debug], [--no-debug] Verbose debugging messages 67 | [--log-level=LOG_LEVEL] Set the log level: info (default), debug, warn, error 68 | [--log-location=LOG_LOCATION] Location to send diagnostic log messages to. (default: STDOUT or Inspec::Log.error) 69 | Note: --resourcepath should point to the directory where inspec- resource pack is downloaded/cloned from GitHub. 70 | 71 | # InSpec Terraform Negative 72 | 73 | inspec terraform negative --tfstate terraform.tfstate --name myprofile --platform aws --resourcepath /tmp/inspec-aws 74 | 75 | Iggy dynamically pulls the available Cloud resources from InSpec and attempts to map them to Terraform resources, producing an InSpec profile which are not part of tfstate file. It informs the user that these resources are not part of tfstate file and can be deleted if not needed.```inspec terraform negative --help``` will show all available options. 76 | 77 | ## Usage 78 | 79 | inspec terraform negative [options] -n, --name=NAME 80 | 81 | -n, --name=NAME Name of profile to be generated (required) 82 | -t, [--tfstate=TFSTATE] Specify path to the input terraform.tfstate (default: .) 83 | --platform=gcp|aws|azure Cloud provider name 84 | --resourcepath=PATH Location of inspec-gcp|inspec-aws|inspec-azure resources 85 | [--copyright=COPYRIGHT] Name of the copyright holder (default: The Authors) 86 | [--email=EMAIL] Email address of the author (default: you@example.com) 87 | [--license=LICENSE] License for the profile (default: Apache-2.0) 88 | [--maintainer=MAINTAINER] Name of the copyright holder (default: The Authors) 89 | [--summary=SUMMARY] One line summary for the profile (default: An InSpec Compliance Profile) 90 | [--title=TITLE] Human-readable name for the profile (default: InSpec Profile) 91 | [--version=VERSION] Specify the profile version (default: 0.1.0) 92 | [--overwrite], [--no-overwrite] Overwrites existing profile directory 93 | [--debug], [--no-debug] Verbose debugging messages 94 | [--log-level=LOG_LEVEL] Set the log level: info (default), debug, warn, error 95 | [--log-location=LOG_LOCATION] Location to send diagnostic log messages to. (default: STDOUT or Inspec::Log.error) 96 | 97 | Note: --resourcepath should point to the directory where inspec- resource pack is downloaded/cloned from GitHub. 98 | 99 | # InSpec CloudFormation Generate 100 | 101 | inspec cloudformation generate --template mytemplate.json --stack mystack-20180909T052147Z --profile myprofile 102 | 103 | Iggy supports AWS CloudFormation templates by mapping the AWS resources to InSpec resources and using the stack name or unique stack ID associated with the CloudFormation template as an entry point to check those resources in the generated profile. ```inspec cloudformation generate --help``` will show all available options. 104 | 105 | ## Usage 106 | 107 | inspec cloudformation generate [options] -n, --name=NAME -s, --stack=STACK -t, --template=TEMPLATE 108 | 109 | -n, --name=NAME Name of profile to be generated (required) 110 | -s, --stack=STACK Specify stack name or unique stack ID associated with the CloudFormation template 111 | -t, --template=TEMPLATE Specify path to the input CloudFormation template 112 | [--copyright=COPYRIGHT] Name of the copyright holder (default: The Authors) 113 | [--email=EMAIL] Email address of the author (default: you@example.com) 114 | [--license=LICENSE] License for the profile (default: Apache-2.0) 115 | [--maintainer=MAINTAINER] Name of the copyright holder (default: The Authors) 116 | [--summary=SUMMARY] One line summary for the profile (default: An InSpec Compliance Profile) 117 | [--title=TITLE] Human-readable name for the profile (default: InSpec Profile) 118 | [--version=VERSION] Specify the profile version (default: 0.1.0) 119 | [--overwrite], [--no-overwrite] Overwrites existing profile directory 120 | [--debug], [--no-debug] Verbose debugging messages 121 | [--log-level=LOG_LEVEL] Set the log level: info (default), debug, warn, error 122 | [--log-location=LOG_LOCATION] Location to send diagnostic log messages to. (default: STDOUT or Inspec::Log.error) 123 | 124 | # InSpec Iggy 125 | 126 | inspec iggy version 127 | 128 | This command exists for checking the Iggy plugin version, primarily for debugging purposes. 129 | 130 | # Development and Testing 131 | 132 | The [DESIGN.md](DESIGN.md) file outlines how the code is structured if you wish to extend functionality. We welcome patches, suggestions, and issues. 133 | 134 | ## Installation 135 | 136 | To point `inspec` at a local copy of `inspec-iggy` for development, use: 137 | 138 | $ inspec plugin install path/to/your/inspec-iggy/lib/inspec-iggy.rb 139 | 140 | ## Testing Iggy 141 | 142 | Unit, Functional, and Integration tests are provided, though more are welcome. Iggy uses the Minitest library for unit testing, using the classic `def test...` syntax. Because Iggy loads InSpec into memory, and InSpec uses RSpec internally, Spec-style testing breaks. For Integration and regression testing Iggy uses InSpec itself for tests (check the Rakefile and [test/inspec](test/inspec) for examples). 143 | 144 | To run all tests, run 145 | 146 | $ bundle exec rake test 147 | 148 | Linting is also provided via [Chefstyle](https://github.com/chef/chefstyle). 149 | 150 | To check for code style issues, run: 151 | 152 | $ bundle exec rake lint 153 | 154 | You can auto-correct many issues: 155 | 156 | $ bundle exec rake lint:auto_correct 157 | 158 | # License and Author # 159 | 160 | | | | 161 | |:---------------|:------------------------------------------| 162 | | **Author** | Matt Ray () | 163 | | **Copyright:** | Copyright (c) 2017-2019 Chef Software Inc.| 164 | | **License:** | Apache License, Version 2.0 | 165 | 166 | Licensed under the Apache License, Version 2.0 (the "License"); 167 | you may not use this file except in compliance with the License. 168 | You may obtain a copy of the License at 169 | 170 | http://www.apache.org/licenses/LICENSE-2.0 171 | 172 | Unless required by applicable law or agreed to in writing, software 173 | distributed under the License is distributed on an "AS IS" BASIS, 174 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 175 | See the License for the specific language governing permissions and 176 | limitations under the License. 177 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------# 2 | # Gem Packaging Tasks 3 | #------------------------------------------------------------------# 4 | begin 5 | require "bundler" 6 | Bundler::GemHelper.install_tasks 7 | rescue LoadError 8 | # no bundler available 9 | end 10 | 11 | #------------------------------------------------------------------# 12 | # Linter Tasks 13 | #------------------------------------------------------------------# 14 | 15 | begin 16 | require "chefstyle" 17 | require "rubocop/rake_task" 18 | RuboCop::RakeTask.new(:lint) do |task| 19 | task.options += ["--display-cop-names", "--no-color", "--parallel"] 20 | end 21 | 22 | rescue LoadError 23 | puts "rubocop is not available. Install the rubocop gem to run the lint tests." 24 | end 25 | 26 | #------------------------------------------------------------------# 27 | # Test Runner Tasks 28 | #------------------------------------------------------------------# 29 | require "rake/testtask" 30 | 31 | namespace(:test) do 32 | # This task template will make a task named 'test', and run 33 | # the tests that it finds. 34 | # Here, we split up the tests a bit, for the convenience 35 | # of the developer. 36 | desc "Run unit tests, to probe internal correctness" 37 | Rake::TestTask.new(:unit) do |task| 38 | task.libs << "test" 39 | task.pattern = "test/unit/*_spec.rb" 40 | task.warning = false 41 | end 42 | 43 | require "tmpdir" 44 | desc "Run InSpec integration tests for check for interface changes" 45 | Rake::TestTask.new(:inspec) do |task| 46 | task.libs << "test" 47 | tmp_dir = Dir.mktmpdir 48 | sh("bundle exec gem build inspec-iggy.gemspec") 49 | sh("bundle exec inspec plugin install inspec-iggy-*.gem --chef-license=accept") 50 | sh("wget -O #{tmp_dir}/inspec-aws.tar.gz -nc --tries=10 https://github.com/inspec/inspec-aws/archive/v1.5.1.tar.gz") 51 | sh("tar -C #{tmp_dir} -xzf #{tmp_dir}/inspec-aws.tar.gz") 52 | sh("bundle exec inspec exec test/inspec --reporter=progress --input tmp_dir='#{tmp_dir}' resource_dir='#{tmp_dir}/inspec-aws-1.5.1'") 53 | FileUtils.remove_dir(tmp_dir) 54 | task.warning = false 55 | end 56 | 57 | end 58 | 59 | # Define a 'run all the tests' task. 60 | # You might think you'd want to depend on test:unit and test:functional, 61 | # but if you do that and either has a failure, the latter won't execute. 62 | desc "Run all tests" 63 | task test: %i{test:unit test:inspec} 64 | -------------------------------------------------------------------------------- /inspec-iggy.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("lib", __dir__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | require "inspec-iggy/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "inspec-iggy" 9 | spec.version = InspecPlugins::Iggy::VERSION 10 | spec.authors = ["Matt Ray"] 11 | spec.email = ["matt@chef.io"] 12 | spec.summary = "InSpec plugin to generate InSpec compliance profiles from Terraform and CloudFormation." 13 | spec.description = "InSpec plugin to generate InSpec profiles from Terraform and CloudFormation to ensure automatic compliance coverage." 14 | spec.homepage = "https://github.com/mattray/inspec-iggy" 15 | spec.license = "Apache-2.0" 16 | 17 | spec.files = %w{ 18 | README.md inspec-iggy.gemspec Gemfile 19 | } + Dir.glob( 20 | "{bin,docs,examples,lib,tasks}/**/*", File::FNM_DOTMATCH 21 | ).reject { |f| File.directory?(f) } 22 | 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_dependency "inspec", ">=3", "<5" 26 | end 27 | -------------------------------------------------------------------------------- /lib/inspec-iggy.rb: -------------------------------------------------------------------------------- 1 | # Next two lines simply add the path of the gem to the load path. 2 | # This is not needed when being loaded as a gem; but when doing 3 | # plugin development, you may need it. Either way, it's harmless. 4 | 5 | libdir = File.dirname(__FILE__) 6 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) 7 | 8 | require "inspec-iggy/plugin" 9 | -------------------------------------------------------------------------------- /lib/inspec-iggy/cloudformation/cli_command.rb: -------------------------------------------------------------------------------- 1 | # CloudFormation CLI command and options 2 | 3 | require "inspec/plugin/v2" 4 | 5 | require "inspec-iggy/version" 6 | require "inspec-iggy/profile_helper" 7 | require "inspec-iggy/cloudformation/generate" 8 | 9 | module InspecPlugins::Iggy 10 | module CloudFormation 11 | class CliCommand < Inspec.plugin(2, :cli_command) 12 | subcommand_desc "cloudformation SUBCOMMAND ...", "Generate an InSpec profile from CloudFormation" 13 | 14 | option :debug, 15 | desc: "Verbose debugging messages", 16 | type: :boolean, 17 | default: false 18 | 19 | option :copyright, 20 | desc: "Name of the copyright holder", 21 | default: "The Authors" 22 | 23 | option :email, 24 | desc: "Email address of the author", 25 | default: "you@example.com" 26 | 27 | option :license, 28 | desc: "License for the profile", 29 | default: "Apache-2.0" 30 | 31 | option :maintainer, 32 | desc: "Name of the copyright holder", 33 | default: "The Authors" 34 | 35 | option :summary, 36 | desc: "One line summary for the profile", 37 | default: "An InSpec Compliance Profile" 38 | 39 | option :title, 40 | desc: "Human-readable name for the profile", 41 | default: "InSpec Profile" 42 | 43 | option :version, 44 | desc: "Specify the profile version", 45 | default: "0.1.0" 46 | 47 | option :overwrite, 48 | desc: "Overwrites existing profile directory", 49 | type: :boolean, 50 | default: false 51 | 52 | option :name, 53 | aliases: "-n", 54 | required: true, 55 | desc: "Name of profile to be generated" 56 | 57 | option :stack, 58 | aliases: "-s", 59 | required: true, 60 | desc: "Specify stack name or unique stack ID associated with the CloudFormation template" 61 | 62 | option :template, 63 | aliases: "-t", 64 | required: true, 65 | desc: "Specify path to the input CloudFormation template" 66 | 67 | desc "generate [options]", "Generate InSpec compliance controls from CloudFormation template" 68 | def generate 69 | Inspec::Log.level = :debug if options[:debug] 70 | # hash of generated controls 71 | generated_controls = InspecPlugins::Iggy::CloudFormation::Generate.parse_generate(options[:template]) 72 | printable_controls = InspecPlugins::Iggy::InspecHelper.cfn_controls(options[:title], generated_controls, options[:stack]) 73 | InspecPlugins::Iggy::ProfileHelper.render_profile(ui, options, options[:template], printable_controls) 74 | exit 0 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/inspec-iggy/cloudformation/generate.rb: -------------------------------------------------------------------------------- 1 | # parses CloudFormation JSON files 2 | 3 | require "inspec/objects/control" 4 | require "inspec/objects/ruby_helper" 5 | require "inspec/objects/describe" 6 | 7 | require "inspec-iggy/file_helper" 8 | require "inspec-iggy/inspec_helper" 9 | 10 | module InspecPlugins::Iggy::CloudFormation 11 | class Generate 12 | # parse through the JSON and generate InSpec controls 13 | def self.parse_generate(cfn_template) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity 14 | template = InspecPlugins::Iggy::FileHelper.parse_json(cfn_template) 15 | absolutename = File.absolute_path(cfn_template) 16 | 17 | # InSpec controls generated 18 | generated_controls = [] 19 | 20 | # iterate over the resources 21 | cfn_resources = template["Resources"] 22 | cfn_resources.keys.each do |cfn_res| 23 | # split out the last ::, these are all AWS 24 | cfn_resource = cfn_resources[cfn_res]["Type"].split("::").last 25 | # split camelcase and join with underscores 26 | cfn_res_type = "aws_" + cfn_resource.split(/(?=[A-Z])/).join("_").downcase 27 | 28 | # add translation layer 29 | if InspecPlugins::Iggy::InspecHelper::TRANSLATED_RESOURCES.key?(cfn_res_type) 30 | Inspec::Log.debug "CloudFormation::Generate.parse_generate cfn_res_type = #{cfn_res_type} #{InspecPlugins::Iggy::InspecHelper::TRANSLATED_RESOURCES[cfn_res_type]} TRANSLATED" 31 | cfn_res_type = InspecPlugins::Iggy::InspecHelper::TRANSLATED_RESOURCES[cfn_res_type] 32 | end 33 | 34 | # does this match an InSpec resource? 35 | if InspecPlugins::Iggy::InspecHelper.available_resources.include?(cfn_res_type) 36 | Inspec::Log.debug "CloudFormation::Generate.parse_generate cfn_res_type = #{cfn_res_type} MATCHED" 37 | 38 | # insert new control based off the resource's ID 39 | ctrl = Inspec::Control.new 40 | ctrl.id = "#{cfn_res_type}::#{cfn_res}" 41 | ctrl.title = "InSpec-Iggy #{cfn_res_type}::#{cfn_res}" 42 | ctrl.descriptions["default"] = "#{cfn_res_type}::#{cfn_res} from the source file #{absolutename}\nGenerated by InSpec-Iggy v#{InspecPlugins::Iggy::VERSION}" 43 | ctrl.impact = "1.0" 44 | 45 | describe = Inspec::Describe.new 46 | # describes the resource with the logical_resource_id as argument, replaced at inspec exec 47 | describe.qualifier.push([cfn_res_type, "resources[#{cfn_res}]"]) 48 | 49 | # ensure the resource exists 50 | describe.add_test(nil, "exist", nil) 51 | 52 | # EC2 instances should be running 53 | describe.add_test(nil, "be_running", nil) if cfn_res_type.eql?("aws_ec2_instance") 54 | 55 | # if there's a match, see if there are matching InSpec properties 56 | inspec_properties = InspecPlugins::Iggy::InspecHelper.resource_properties(cfn_res_type, "aws") 57 | cfn_resources[cfn_res]["Properties"].keys.each do |attr| 58 | # insert '_' on the CamelCase to get camel_case 59 | attr_split = attr.split(/(?=[A-Z])/) 60 | property = attr_split.join("_").downcase 61 | if inspec_properties.member?(property) 62 | Inspec::Log.debug "CloudFormation::Generate.parse_generate #{cfn_res_type} inspec_property = #{property} MATCHED" 63 | value = cfn_resources[cfn_res]["Properties"][attr] 64 | if (value.is_a? Hash) || (value.is_a? Array) 65 | # these get replaced at inspec exec 66 | if property.eql?("vpc_id") # rubocop:disable Metrics/BlockNesting 67 | vpc = cfn_resources[cfn_res]["Properties"][attr].values.first 68 | # https://github.com/inspec/inspec/issues/3173 69 | describe.add_test(property, "cmp", "resources[#{vpc}]") unless cfn_res_type.eql?("aws_route_table") # rubocop:disable Metrics/BlockNesting 70 | # AMI is a Ref into Parameters 71 | elsif property.eql?("image_id") # rubocop:disable Metrics/BlockNesting 72 | amiref = cfn_resources[cfn_res]["Properties"][attr].values.first 73 | ami = template["Parameters"][amiref]["Default"] 74 | describe.add_test(property, "cmp", ami) 75 | end 76 | else 77 | describe.add_test(property, "cmp", value) 78 | end 79 | else 80 | Inspec::Log.debug "CloudFormation::Generate.parse_generate #{cfn_res_type} inspec_property = #{property} SKIPPED" 81 | end 82 | end 83 | ctrl.add_test(describe) 84 | generated_controls.push(ctrl) 85 | else 86 | Inspec::Log.debug "CloudFormation::Generate.parse_generate cfn_res_type = #{cfn_res_type} SKIPPED" 87 | end 88 | end 89 | Inspec::Log.debug "CloudFormation::Generate.parse_generate generated_controls = #{generated_controls}" 90 | generated_controls 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/inspec-iggy/file_helper.rb: -------------------------------------------------------------------------------- 1 | # helper methods for retrieving and parsing files 2 | 3 | require "json" 4 | require "open-uri" 5 | 6 | module InspecPlugins 7 | module Iggy 8 | class FileHelper 9 | # boilerplate JSON parsing 10 | def self.parse_json(file) 11 | Inspec::Log.debug "Iggy::FileHelper.parse_json file = #{file}" 12 | lfile = fetch(file) 13 | begin 14 | unless File.file?(lfile) 15 | STDERR.puts "ERROR: #{lfile} is an invalid file, please check your path." 16 | exit(-1) 17 | end 18 | JSON.parse(File.read(lfile)) 19 | rescue JSON::ParserError => e 20 | STDERR.puts e.message 21 | STDERR.puts "ERROR: Parsing error in #{lfile}." 22 | exit(-1) 23 | end 24 | end 25 | 26 | def self.fetch(url) 27 | # if this is a file, just return it 28 | return url if File.exist?(url) 29 | 30 | begin 31 | URI.parse(url).open 32 | rescue OpenURI::HTTPError => e 33 | STDERR.puts e.message 34 | STDERR.puts "ERROR: Parsing error from URL #{url}" 35 | exit(-1) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/inspec-iggy/iggy_cli_command.rb: -------------------------------------------------------------------------------- 1 | # CloudFormation CLI command and options 2 | 3 | require "inspec/plugin/v2" 4 | 5 | require "inspec-iggy/version" 6 | 7 | module InspecPlugins 8 | module Iggy 9 | class CliCommand < Inspec.plugin(2, :cli_command) 10 | subcommand_desc "iggy", "Use 'inspec cloudformation' or 'inspec terraform'" 11 | 12 | desc "version", "Display version information" 13 | def version 14 | say("Iggy v#{InspecPlugins::Iggy::VERSION}") 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/inspec-iggy/inspec_helper.rb: -------------------------------------------------------------------------------- 1 | # constants and helpers for working with InSpec 2 | 3 | require "inspec" 4 | 5 | require "inspec-iggy/platforms/aws_helper" 6 | require "inspec-iggy/platforms/azure_helper" 7 | require "inspec-iggy/platforms/gcp_helper" 8 | 9 | module InspecPlugins 10 | module Iggy 11 | class InspecHelper 12 | @inspec_resources = Inspec::Resource.registry.keys 13 | 14 | # list of resources available from InSpec 15 | def self.available_resources 16 | @inspec_resources 17 | end 18 | 19 | # translate Terraform resource name to InSpec 20 | TRANSLATED_RESOURCES = { 21 | "aws_instance" => "aws_ec2_instance", 22 | "aws_v_p_c" => "aws_vpc", # CFN 23 | "azurerm_resource_group" => "azure_resource_group", 24 | "azurerm_virtual_machine" => "azure_virtual_machine", 25 | # "azure_virtual_machine_data_disk", 26 | # 'aws_route' => 'aws_route_table' # needs route_table_id instead of id 27 | }.freeze 28 | 29 | def self.available_resource_qualifiers(platform) 30 | case platform 31 | when "aws" 32 | InspecPlugins::Iggy::Platforms::AwsHelper::AWS_RESOURCE_QUALIFIERS 33 | when "azure" 34 | InspecPlugins::Iggy::Platforms::AzureHelper::AZURE_RESOURCE_QUALIFIERS 35 | when "gcp" 36 | InspecPlugins::Iggy::Platforms::GcpHelper::GCP_RESOURCE_QUALIFIERS 37 | end 38 | end 39 | 40 | def self.available_resource_iterators(platform) 41 | case platform 42 | when "aws" 43 | InspecPlugins::Iggy::Platforms::AwsHelper::AWS_RESOURCE_ITERATORS 44 | when "azure" 45 | InspecPlugins::Iggy::Platforms::AzureHelper::AZURE_RESOURCE_ITERATORS 46 | when "gcp" 47 | InspecPlugins::Iggy::Platforms::GcpHelper::GCP_RESOURCE_ITERATORS 48 | end 49 | end 50 | 51 | def self.available_translated_resource_properties(platform, resource) 52 | case platform 53 | when "aws" 54 | InspecPlugins::Iggy::Platforms::AwsHelper::AWS_TRANSLATED_RESOURCE_PROPERTIES[resource] 55 | when "azure" 56 | InspecPlugins::Iggy::Platforms::AzureHelper::AZURE_TRANSLATED_RESOURCE_PROPERTIES[resource] 57 | when "gcp" 58 | InspecPlugins::Iggy::Platforms::GcpHelper::GCP_TRANSLATED_RESOURCE_PROPERTIES[resource] 59 | end 60 | end 61 | 62 | def self.translated_resource_property(platform, resource, property) 63 | translated_resource = available_translated_resource_properties(platform, resource) 64 | translated_property = translated_resource[property] if translated_resource 65 | if translated_property 66 | Inspec::Log.debug "InspecHelper.translated_resource_property #{platform}:#{resource}:#{property} = #{translated_property} TRANSLATED" 67 | translated_property 68 | else 69 | property 70 | end 71 | end 72 | 73 | # properties are often dynamically generated, making it hard to determine 74 | # their existence without instantiating them. Because of this, we will 75 | # maintain a manual list for now 76 | ADDITIONAL_COMMON_PROPERTIES = [ 77 | # :id, #disabled for GCP 78 | # :tags, # returns emtpy hashes when null 79 | :addons_config, 80 | :address, 81 | :address_type, 82 | :aggregation_alignment_period, 83 | :aggregation_cross_series_reducer, 84 | :aggregation_per_series_aligner, 85 | :allowed, 86 | :archive_size_bytes, 87 | :associations, 88 | :auto_create_subnetworks, 89 | :availability_zone, 90 | :availability_zones, 91 | :available_cpu_platforms, 92 | :available_ip_address_count, 93 | :available_memory_mb, 94 | :backend_service, 95 | :backup_pool, 96 | :base_instance_name, 97 | :can_ip_forward, 98 | :canonical_hosted_zone_id, 99 | :capabilities, 100 | :change_set_id, 101 | :check_interval_sec, 102 | :cidr_block, 103 | :cloud_watch_logs_log_group_arn, 104 | :cloud_watch_logs_role_arn, 105 | :cluster_ipv4_cidr, 106 | :combiner, 107 | :common_instance_metadata, 108 | :condition_threshold_value, 109 | :conditions, 110 | :config, 111 | :cpu_platform, 112 | :create_time, 113 | :create_time_date, 114 | :created_time, 115 | :creation_record, 116 | :creation_time, 117 | :creation_timestamp, 118 | :creation_timestamp_date, 119 | :crypto_key_name, 120 | :crypto_key_url, 121 | :current_actions, 122 | :current_master_version, 123 | :current_node_count, 124 | :current_node_version, 125 | :custom_features, 126 | :dataset, 127 | :dataset_id, 128 | :default_exempted_members, 129 | :default_service_account, 130 | :default_types, 131 | :deletion_protection, 132 | :deletion_time, 133 | :description, 134 | :desired_capacity, 135 | :detailed_status, 136 | :dhcp_options_id, 137 | :direction, 138 | :disable_rollback, 139 | :disabled, 140 | :disk_encryption_key, 141 | :disk_size_gb, 142 | :disks, 143 | :display_name, 144 | :dns_name, 145 | :dnssec_config, 146 | :drift_information, 147 | :ebs_volumes, 148 | :enable_termination_protection, 149 | :enabled, 150 | :enabled_features, 151 | :endpoint, 152 | :entry_point, 153 | :environment_variables, 154 | :etag, 155 | :expire_time, 156 | :external_ports, 157 | :failover_ratio, 158 | :family, 159 | :filename, 160 | :filter, 161 | :fingerprint, 162 | :friendly_name, 163 | :gateway_address, 164 | :group_id, 165 | :group_name, 166 | :guest_accelerators, 167 | :guest_os_features, 168 | :health_check, 169 | :health_check_type, 170 | :healthy_threshold, 171 | :home_region, 172 | :host, 173 | :ignored_files, 174 | :ike_version, 175 | :image_id, 176 | :inbound_rules, 177 | :inbound_rules_count, 178 | :included_files, 179 | :included_permissions, 180 | :initial_cluster_version, 181 | :initial_node_count, 182 | :instance_group, 183 | :instance_group_urls, 184 | :instance_ids, 185 | :instance_template, 186 | :instance_tenancy, 187 | :internal_ports, 188 | :ip_address, 189 | :ip_cidr_range, 190 | :ip_protocol, 191 | :ip_version, 192 | :is_multi_region_trail, 193 | :key_ring_name, 194 | :key_ring_url, 195 | :key_signing_key_algorithm, 196 | :kind, 197 | :kms_key_id, 198 | :kms_key_name, 199 | :label_fingerprint, 200 | :label_value_by_key, 201 | :labels, 202 | :labels_keys, 203 | :labels_values, 204 | :last_attach_timestamp, 205 | :last_detach_timestamp, 206 | :last_modified_time, 207 | :last_updated_time, 208 | :launch_configuration_name, 209 | :launch_time, 210 | :legacy_abac, 211 | :licenses, 212 | :lifecycle_state, 213 | :load_balancer_addresses, 214 | :load_balancer_arn, 215 | :load_balancer_name, 216 | :load_balancing_scheme, 217 | :local_traffic_selector, 218 | :location, 219 | :log_file_validation_enabled, 220 | :logging_service, 221 | :machine_type, 222 | :managed_zone, 223 | :management, 224 | :master_auth, 225 | :max_size, 226 | :members, 227 | :metadata, 228 | :metadata_keys, 229 | :metadata_value_by_key, 230 | :metadata_values, 231 | :min_cpu_platform, 232 | :min_size, 233 | :monitoring_service, 234 | :mutation_record, 235 | :name, 236 | :name_servers, 237 | :named_ports, 238 | :network, 239 | :network_interfaces, 240 | :next_hop_gateway, 241 | :next_hop_instance, 242 | :next_hop_ip, 243 | :next_hop_network, 244 | :next_hop_vpn_tunnel, 245 | :next_rotation_time, 246 | :next_rotation_time_date, 247 | :node_config, 248 | :node_ipv4_cidr_size, 249 | :node_pools, 250 | :notification_arns, 251 | :num_bytes, 252 | :num_long_term_bytes, 253 | :num_rows, 254 | :outbound_rules, 255 | :outbound_rules_count, 256 | :output_version_format, 257 | :outputs, 258 | :owner_id, 259 | :parameters, 260 | :parent, 261 | :parent_id, 262 | :peer_ip, 263 | :physical_block_size_bytes, 264 | :port, 265 | :port_range, 266 | :ports, 267 | :primary_create_time, 268 | :primary_create_time_date, 269 | :primary_name, 270 | :primary_state, 271 | :priority, 272 | :private_ip_google_access, 273 | :private_key, 274 | :profile, 275 | :project_id, 276 | :project_number, 277 | :propagating_vgws, 278 | :protocol, 279 | :proxy_header, 280 | :purpose, 281 | :quic_override, 282 | :quotas, 283 | :raw_disk, 284 | :raw_key, 285 | :region, 286 | :region_name, 287 | :remote_traffic_selector, 288 | :request_path, 289 | :role_arn, 290 | :rollback_configuration, 291 | :root_id, 292 | :rotation_period, 293 | :router, 294 | :routes, 295 | :routing_config, 296 | :runtime, 297 | :s3_bucket_name, 298 | :scheduling, 299 | :scheme, 300 | :security_group_ids, 301 | :security_groups, 302 | :self_link, 303 | :service, 304 | :service_account_email, 305 | :service_accounts, 306 | :services_ipv4_cidr, 307 | :session_affinity, 308 | :sha256, 309 | :shared_secret, 310 | :shared_secret_hash, 311 | :size_gb, 312 | :source_archive_url, 313 | :source_disk, 314 | :source_image, 315 | :source_image_encryption_key, 316 | :source_image_id, 317 | :source_ranges, 318 | :source_snapshot, 319 | :source_snapshot_encryption_key, 320 | :source_snapshot_id, 321 | :source_type, 322 | :source_upload_url, 323 | :ssl_certificates, 324 | :ssl_policy, 325 | :stack_id, 326 | :stack_name, 327 | :stack_status, 328 | :stack_status_reason, 329 | :stage, 330 | :start_restricted, 331 | :state, 332 | :status, 333 | :storage_bytes, 334 | :subnet_id, 335 | :subnet_ids, 336 | :subnets, 337 | :subnetwork, 338 | :substitutions, 339 | :table_id, 340 | :table_reference, 341 | :target, 342 | :target_pools, 343 | :target_size, 344 | :target_tags, 345 | :target_vpn_gateway, 346 | :timeout, 347 | :timeout_in_minutes, 348 | :timeout_sec, 349 | :title, 350 | :trail_arn, 351 | :trail_name, 352 | :ttl, 353 | :type, 354 | :unhealthy_threshold, 355 | :update_time, 356 | :url_map, 357 | :users, 358 | :version, 359 | :version_id, 360 | :vpc_id, 361 | :vpc_zone_identifier, 362 | :writer_identity, 363 | :xpn_project_status, 364 | :zone, 365 | :zone_names, 366 | :zone_signing_key_algorithm, 367 | ].freeze 368 | 369 | # load the resource pack into InSpec::Resource.registry 370 | def self.load_resource_pack(resource_path) 371 | # find the libraries path in the resource pack 372 | if resource_path.end_with?("libraries") 373 | libpath = resource_path 374 | else 375 | libpath = resource_path + "/libraries" 376 | end 377 | $LOAD_PATH.push(libpath) 378 | # find all the classes in the libpath and require them 379 | # this adds them to the Inspec::Resource.registry 380 | Dir.glob("#{libpath}/*.rb").each do |x| 381 | begin 382 | require(x) 383 | rescue Exception => e # rubocop:disable Lint/RescueException AWS is blowing up for some reason 384 | puts e 385 | end 386 | end 387 | @inspec_resources = Inspec::Resource.registry.keys 388 | end 389 | 390 | # there really should be some way to get this directly from InSpec's resources 391 | def self.resource_properties(resource, platform) 392 | # remove the common methods, in theory only leaving only unique InSpec properties 393 | inspec_properties = Inspec::Resource.registry[resource].instance_methods - Inspec::Resource.registry[resource].methods 394 | inspec_properties += ADDITIONAL_COMMON_PROPERTIES 395 | case platform 396 | when "aws" 397 | inspec_properties -= InspecPlugins::Iggy::Platforms::AwsHelper::AWS_REMOVED_PROPERTIES[resource] unless InspecPlugins::Iggy::Platforms::AwsHelper::AWS_REMOVED_PROPERTIES[resource].nil? 398 | when "azure" 399 | inspec_properties -= InspecPlugins::Iggy::Platforms::AzureHelper::AZURE_REMOVED_PROPERTIES[resource] unless InspecPlugins::Iggy::Platforms::AzureHelper::AZURE_REMOVED_PROPERTIES[resource].nil? 400 | when "gcp" 401 | inspec_properties -= InspecPlugins::Iggy::Platforms::GcpHelper::GCP_REMOVED_PROPERTIES[resource] unless InspecPlugins::Iggy::Platforms::GcpHelper::GCP_REMOVED_PROPERTIES[resource].nil? 402 | end 403 | # get InSpec properties by method names 404 | inspec_properties.collect!(&:to_s) 405 | Inspec::Log.debug "InspecHelper.resource_properties #{resource} properties = #{inspec_properties}" 406 | 407 | inspec_properties 408 | end 409 | 410 | def self.tf_controls(title, generated_controls, platform) 411 | content = "title \"#{title}: generated by Iggy v#{Iggy::VERSION}\"\n" 412 | 413 | content += InspecPlugins::Iggy::Platforms::AwsHelper.tf_controls if platform.eql?("aws") 414 | 415 | # write all controls 416 | generated_controls.flatten.each do |control| 417 | if control.class.eql?(Inspec::Control) 418 | content += control.to_ruby 419 | content += "\n\n" 420 | else # this is for embedded iterators in negative tests 421 | content += control 422 | end 423 | end 424 | content 425 | end 426 | 427 | def self.cfn_controls(title, generated_controls, stack) 428 | content = "# encoding: utf-8\n#\n\n" 429 | 430 | content += "begin\n" 431 | content += " awsclient = Aws::CloudFormation::Client.new()\n" 432 | content += " cfn = awsclient.list_stack_resources({ stack_name: \"#{stack}\" }).to_hash\n" 433 | content += " resources = {}\n" 434 | content += " cfn[:stack_resource_summaries].each { |r| resources[r[:logical_resource_id]] = r[:physical_resource_id] }\n" 435 | content += "rescue Exception => e\n" 436 | content += " raise(e) unless @conf['profile'].check_mode\n" 437 | content += "end\n\n" 438 | 439 | content += "title \"#{title}: generated by Iggy v#{Iggy::VERSION}\"\n" 440 | 441 | # get the controls, insert lookups for physical_resource_ids 442 | controls = generated_controls.flatten.map(&:to_ruby).join("\n\n") 443 | controls.gsub!(/\"resources\[/, 'resources["') 444 | controls.gsub!(/\]\"/, '"]') 445 | content + controls 446 | end 447 | end 448 | end 449 | end 450 | -------------------------------------------------------------------------------- /lib/inspec-iggy/platforms/aws_helper.rb: -------------------------------------------------------------------------------- 1 | # helpers for working with InSpec-AWS profiles 2 | 3 | require "yaml" 4 | 5 | module InspecPlugins::Iggy::Platforms 6 | class AwsHelper 7 | # find the additional parameters for the 'describe'. 8 | # NOTE: the first entry is going to map to the 'id' from the .tfstate file 9 | AWS_RESOURCE_QUALIFIERS = { 10 | "aws_alb" => %i{load_balancer_name}, 11 | "aws_cloudformation_stack" => %i{stack_id}, 12 | "aws_cloudtrail_trail" => %i{trail_name}, 13 | "aws_ec2_instance" => %i{instance_id}, 14 | "aws_elb" => %i{load_balancer_name}, 15 | "aws_route_table" => %i{route_table_id}, 16 | "aws_security_group" => %i{group_id vpc_id}, 17 | "aws_subnet" => %i{subnet_id}, 18 | "aws_vpc" => %i{vpc_id}, 19 | }.freeze 20 | 21 | # the iterators for the various resource types 22 | AWS_RESOURCE_ITERATORS = { 23 | "aws_auto_scaling_group" => { "iterator" => "aws_auto_scaling_groups", "index" => "names" }, 24 | "aws_cloudtrail_trail" => { "iterator" => "aws_cloudtrail_trails", "index" => "names" }, 25 | "aws_ec2_instance" => { "iterator" => "aws_ec2_instances", "index" => "instance_ids", "qualifiers" => [:vpc_id] }, 26 | "aws_elb" => { "iterator" => "aws_elbs", "index" => "load_balancer_names", "qualifiers" => [:vpc_id] }, 27 | "aws_route_table" => { "iterator" => "aws_route_tables", "index" => "route_table_ids", "qualifiers" => [:vpc_id] }, 28 | "aws_security_group" => { "iterator" => "aws_security_groups", "index" => "group_ids", "qualifiers" => [:vpc_id] }, 29 | "aws_subnet" => { "iterator" => "aws_subnets", "index" => "subnet_ids", "qualifiers" => [:vpc_id] }, 30 | "aws_vpc" => { "iterator" => "aws_vpcs", "index" => "vpc_ids" }, 31 | }.freeze 32 | 33 | AWS_REMOVED_PROPERTIES = { 34 | "aws_ec2_instance" => %i{security_groups}, # not sure how to test this yet 35 | "aws_elb" => %i{health_check security_groups}, # not sure how to test this yet 36 | "aws_security_group" => %i{owner_id tags}, # tags are {} instead of nil 37 | }.freeze 38 | 39 | AWS_TRANSLATED_RESOURCE_PROPERTIES = { 40 | "aws_alb" => { "name" => "load_balancer_name" }, 41 | "aws_cloudtrail_trail" => { "name" => "trail_name" }, 42 | "aws_elb" => { "name" => "load_balancer_name" }, 43 | "aws_security_group" => { "name" => "group_name" }, 44 | }.freeze 45 | 46 | # Terraform boilerplate controls/controls.rb content 47 | def self.tf_controls 48 | "\n" 49 | end 50 | 51 | # readme content 52 | def self.readme; end 53 | 54 | # inspec.yml boilerplate content from 55 | # inspec/lib/plugins/inspec-init/templates/profiles/aws/inspec.yml 56 | def self.inspec_yml 57 | yml = {} 58 | yml["inspec_version"] = "~> 4" 59 | yml["depends"] = [{ 60 | "name" => "inspec-aws", 61 | "url" => "https://github.com/inspec/inspec-aws/archive/master.tar.gz", 62 | }] 63 | yml["supports"] = [{ 64 | "platform" => "aws", 65 | }] 66 | yml 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/inspec-iggy/platforms/azure_helper.rb: -------------------------------------------------------------------------------- 1 | # helpers for working with InSpec-Azure profiles 2 | 3 | require "yaml" 4 | 5 | module InspecPlugins::Iggy::Platforms 6 | class AzureHelper 7 | # find the additional parameters 8 | AZURE_RESOURCE_QUALIFIERS = { 9 | }.freeze 10 | 11 | # the iterators for the various resource types 12 | AZURE_RESOURCE_ITERATORS = { 13 | }.freeze 14 | 15 | AZURE_REMOVED_PROPERTIES = { 16 | }.freeze 17 | 18 | AZURE_TRANSLATED_RESOURCE_PROPERTIES = { 19 | }.freeze 20 | 21 | # readme content 22 | def self.readme 23 | "\n" 24 | end 25 | 26 | # inspec.yml boilerplate content from 27 | # inspec/lib/plugins/inspec-init/templates/profiles/azure/inspec.yml 28 | def self.inspec_yml 29 | yml = {} 30 | yml["inspec_version"] = ">= 2.2.7" 31 | yml["depends"] = [{ 32 | "name" => "inspec-azure", 33 | "url" => "https://github.com/inspec/inspec-azure/archive/master.tar.gz", 34 | }] 35 | yml["supports"] = [{ 36 | "platform" => "azure", 37 | }] 38 | yml 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/inspec-iggy/platforms/gcp_helper.rb: -------------------------------------------------------------------------------- 1 | # helpers for working with InSpec-GCP profiles 2 | 3 | require "yaml" 4 | 5 | module InspecPlugins::Iggy::Platforms 6 | class GcpHelper 7 | # find the additional parameters for the 'describe' 8 | GCP_RESOURCE_QUALIFIERS = { 9 | "google_bigquery_dataset" => %i{project name}, 10 | "google_bigquery_table" => %i{project dataset name}, 11 | "google_cloudfunctions_cloud_function" => %i{project location name}, 12 | "google_compute_address" => %i{project location name}, 13 | "google_compute_autoscaler" => %i{project zone name}, 14 | "google_compute_backend_bucket" => %i{project name}, 15 | "google_compute_backend_service" => %i{project name}, 16 | "google_compute_disk" => %i{project name zone}, 17 | "google_compute_firewall" => %i{project name}, 18 | "google_compute_forwarding_rule" => %i{project region name}, 19 | "google_compute_global_address" => %i{project name}, 20 | "google_compute_global_forwarding_rule" => %i{project name}, 21 | "google_compute_health_check" => %i{project name}, 22 | "google_compute_http_health_check" => %i{project name}, 23 | "google_compute_https_health_check" => %i{project name}, 24 | "google_compute_image" => %i{project name}, 25 | "google_compute_instance" => %i{project zone name}, 26 | "google_compute_instance_group" => %i{project zone name}, 27 | "google_compute_instance_group_manager" => %i{project zone name}, 28 | "google_compute_instance_template" => %i{project name}, 29 | "google_compute_network" => %i{project name}, 30 | "google_compute_project_info" => [:project], 31 | "google_compute_region" => %i{project name}, 32 | "google_compute_region_backend_service" => %i{project region name}, 33 | "google_compute_region_instance_group_manager" => %i{project region name}, 34 | "google_compute_route" => %i{project name}, 35 | "google_compute_router" => %i{project region name}, 36 | "google_compute_snapshot" => %i{project name}, 37 | "google_compute_ssl_certificate" => %i{project name}, 38 | "google_compute_ssl_policy" => %i{project name}, 39 | "google_compute_subnetwork" => %i{project region name}, 40 | "google_compute_subnetwork_iam_policy" => %i{project region name}, 41 | "google_compute_target_http_proxy" => %i{project name}, 42 | "google_compute_target_https_proxy" => %i{project name}, 43 | "google_compute_target_pool" => %i{project region name}, 44 | "google_compute_target_tcp_proxy" => %i{project name}, 45 | "google_compute_url_map" => %i{project name}, 46 | "google_compute_vpn_tunnel" => %i{project region name}, 47 | "google_compute_zone" => %i{project zone}, 48 | "google_container_cluster" => %i{project zone name}, 49 | "google_container_node_pool" => %i{project zone cluster_name nodepool_name}, 50 | "google_container_regional_cluster" => %i{project location name}, 51 | "google_container_regional_node_pool" => %i{project location cluster name}, 52 | "google_dns_managed_zone" => %i{project zone}, 53 | "google_dns_resource_record_set" => %i{project name type managed_zone}, 54 | "google_kms_crypto_key" => %i{project location key_ring_name name}, 55 | "google_kms_crypto_key_iam_binding" => %i{crypto_key_url role}, 56 | "google_kms_key_ring" => %i{project location name}, 57 | "google_kms_key_ring_iam_binding" => %i{key_ring_url role}, 58 | "google_logging_project_exclusion" => %i{project exclusion}, 59 | "google_logging_project_sink" => %i{project sink}, 60 | "google_organization" => [:display_name], 61 | "google_organization_policy" => %i{name constraints}, 62 | "google_project" => [:project], 63 | "google_project_alert_policy" => [:policy], 64 | "google_project_alert_policy_condition" => %i{name filter}, 65 | "google_project_iam_binding" => %i{project role}, 66 | "google_project_iam_custom_role" => %i{project name}, 67 | "google_project_logging_audit_config" => [:project], 68 | "google_project_metric" => %i{project metric}, 69 | "google_pubsub_subscription" => %i{project name}, 70 | "google_pubsub_subscription_iam_policy" => %i{project name}, 71 | "google_pubsub_topic" => %i{project name}, 72 | "google_pubsub_topic_iam_policy" => %i{project name}, 73 | "google_resourcemanager_organization_policy" => %i{organization_name constraint}, 74 | "google_service_account" => [:name], 75 | "google_service_account_key" => [:name], 76 | "google_sourcerepo_repository" => %i{project name}, 77 | "google_sql_database_instance" => %i{project database}, 78 | "google_storage_bucket" => [:name], 79 | "google_storage_bucket_acl" => %i{bucket entity}, 80 | "google_storage_bucket_iam_binding" => %i{bucket role}, 81 | "google_storage_bucket_object" => %i{bucket object}, 82 | "google_storage_default_object_acl" => %i{bucket entity}, 83 | "google_storage_object_acl" => %i{bucket object entity}, 84 | "google_user" => [:user_key], 85 | }.freeze 86 | 87 | # the iterators for the various resource types 88 | GCP_RESOURCE_ITERATORS = { 89 | # 'google_compute_disk' => { 'iterator' => 'google_compute_disks', 'index' => 'names', 'qualifiers' => [:project, :zone] }, # false positives because instance attached disks aren't managed by Terraform 90 | # 'google_compute_network' => { 'iterator' => 'google_compute_networks', 'index' => 'network_names', 'qualifiers' => [:project] }, 91 | # 'google_compute_region' => { 'iterator' => 'google_compute_regions', 'index' => 'region_names', 'qualifiers' => [:project] }, 92 | # 'google_compute_region_instance_group_manager' => { 'iterator' => 'google_compute_region_instance_group_managers', 'index' => 'instance_group_names', 'qualifiers' => [:project, :region] }, verify it has 2 filter criteria 93 | # 'google_compute_route' => { 'iterator' => 'google_compute_routes', 'index' => 'names', 'qualifiers' => [:project] }, 94 | # 'google_compute_subnetwork' => { 'iterator' => 'google_compute_subnetworks', 'index' => 'subnetwork_names', 'qualifiers' => [:project, :region] }, 95 | # 'google_compute_zone' => { 'iterator' => 'google_compute_zones', 'index' => 'zone_names', 'qualifiers' => [:project] }, 96 | # 'google_kms_crypto_key_iam_binding' => { 'iterator' => 'google_kms_crypto_key_iam_bindings', 'index' => 'iam_binding_roles', 'qualifiers' => [:crypto_key_url] }, 97 | # 'google_kms_key_ring' => { 'iterator' => 'google_kms_key_rings', 'index' => 'key_ring_names', 'qualifiers' => [:project, :location] }, 98 | # 'google_kms_key_ring_iam_binding' => { 'iterator' => 'google_kms_key_ring_iam_bindings', 'index' => 'iam_binding_roles', 'qualifiers' => [:key_ring_url] }, 99 | # 'google_organization' => { 'iterator' => 'google_organizations', 'index' => 'names', 'qualifiers' => [] }, # organizations are not managed by Terraform 100 | # 'google_project' => { 'iterator' => 'google_projects', 'index' => 'project_names', 'qualifiers' => [] }, # projects are not managed by Terraform 101 | # 'google_project_iam_binding' => { 'iterator' => 'google_project_iam_bindings', 'index' => 'iam_binding_roles', 'qualifiers' => [:project] }, 102 | "google_bigquery_dataset" => { "iterator" => "google_bigquery_datasets", "index" => "names", "qualifiers" => [:project] }, 103 | "google_bigquery_table" => { "iterator" => "google_bigquery_tables", "index" => "table_references", "qualifiers" => %i{project dataset} }, 104 | "google_cloudbuild_trigger" => { "iterator" => "google_cloudbuild_triggers", "index" => "names", "qualifiers" => [:project] }, 105 | "google_cloudfunctions_cloud_function" => { "iterator" => "google_cloudfunctions_cloud_functions", "index" => "names", "qualifiers" => %i{project location} }, 106 | "google_compute_autoscaler" => { "iterator" => "google_compute_autoscalers", "index" => "names", "qualifiers" => %i{project zone} }, 107 | "google_compute_backend_bucket" => { "iterator" => "google_compute_backend_buckets", "index" => "names", "qualifiers" => [:project] }, 108 | "google_compute_backend_service" => { "iterator" => "google_compute_backend_services", "index" => "names", "qualifiers" => [:project] }, 109 | "google_compute_firewall" => { "iterator" => "google_compute_firewalls", "index" => "firewall_names", "qualifiers" => [:project] }, 110 | "google_compute_forwarding_rule" => { "iterator" => "google_compute_forwarding_rules", "index" => "forwarding_rule_names", "qualifiers" => %i{project region} }, 111 | "google_compute_health_check" => { "iterator" => "google_compute_health_checks", "index" => "names", "qualifiers" => [:project] }, 112 | "google_compute_http_health_check" => { "iterator" => "google_compute_http_health_checks", "index" => "names", "qualifiers" => [:project] }, 113 | "google_compute_https_health_check" => { "iterator" => "google_compute_https_health_checks", "index" => "names", "qualifiers" => [:project] }, 114 | "google_compute_instance" => { "iterator" => "google_compute_instances", "index" => "instance_names", "qualifiers" => %i{project zone} }, 115 | "google_compute_instance_group" => { "iterator" => "google_compute_instance_groups", "index" => "instance_group_names", "qualifiers" => %i{project zone} }, 116 | "google_compute_instance_group_manager" => { "iterator" => "google_compute_instance_group_managers", "index" => "base_instance_names", "qualifiers" => %i{project zone} }, 117 | "google_compute_instance_template" => { "iterator" => "google_compute_instance_templates", "index" => "names", "qualifiers" => [:project] }, 118 | "google_compute_router" => { "iterator" => "google_compute_routers", "index" => "names", "qualifiers" => %i{project region} }, 119 | "google_compute_snapshot" => { "iterator" => "google_compute_snapshots", "index" => "names", "qualifiers" => [:project] }, 120 | "google_compute_ssl_certificate" => { "iterator" => "google_compute_ssl_certificates", "index" => "names", "qualifiers" => [:project] }, 121 | "google_compute_ssl_policy" => { "iterator" => "google_compute_ssl_policies", "index" => "names", "qualifiers" => [:project] }, 122 | "google_compute_target_http_proxy" => { "iterator" => "google_compute_target_http_proxies", "index" => "names", "qualifiers" => [:project] }, 123 | "google_compute_target_https_proxy" => { "iterator" => "google_compute_target_https_proxies", "index" => "names", "qualifiers" => [:project] }, 124 | "google_compute_target_pool" => { "iterator" => "google_compute_target_pools", "index" => "names", "qualifiers" => %i{project region} }, 125 | "google_compute_target_tcp_proxy" => { "iterator" => "google_compute_target_tcp_proxies", "index" => "names", "qualifiers" => [:project] }, 126 | "google_compute_url_map" => { "iterator" => "google_compute_url_maps", "index" => "names", "qualifiers" => [:project] }, 127 | "google_compute_vpn_tunnel" => { "iterator" => "google_compute_vpn_tunnels", "index" => "vpn_tunnel_names", "qualifiers" => %i{project region} }, 128 | "google_container_cluster" => { "iterator" => "google_container_clusters", "index" => "cluster_names", "qualifiers" => %i{project zone} }, 129 | "google_container_node_pool" => { "iterator" => "google_container_node_pools", "index" => "node_pool_names", "qualifiers" => %i{project zone cluster_name} }, 130 | "google_container_regional_cluster" => { "iterator" => "google_container_regional_clusters", "index" => "names", "qualifiers" => %i{project location} }, 131 | "google_dns_managed_zone" => { "iterator" => "google_dns_managed_zones", "index" => "zone_names", "qualifiers" => [:project] }, 132 | "google_dns_resource_record_set" => { "iterator" => "google_dns_resource_record_sets", "index" => "names", "qualifiers" => %i{project managed_zone} }, 133 | "google_kms_crypto_key" => { "iterator" => "google_kms_crypto_keys", "index" => "crypto_key_names", "qualifiers" => %i{project location key_ring_name} }, 134 | "google_logging_project_sink" => { "iterator" => "google_logging_project_sinks", "index" => "sink_names", "qualifiers" => [:project] }, 135 | "google_project_alert_policy" => { "iterator" => "google_project_alert_policies", "index" => "policy_names", "qualifiers" => [:project] }, 136 | "google_project_metric" => { "iterator" => "google_project_metrics", "index" => "metric_names", "qualifiers" => [:project] }, 137 | "google_pubsub_subscription" => { "iterator" => "google_pubsub_subscriptions", "index" => "names", "qualifiers" => [:project] }, 138 | }.freeze 139 | 140 | GCP_REMOVED_PROPERTIES = { 141 | "google_compute_http_health_check" => %i{self_link id creation_timestamp}, # id: terraform has name not id, self_link: undocumented but broken, creation_timestamp api incompatibility 142 | "google_compute_instance" => %i{label_fingerprint machine_type min_cpu_platform zone}, # label_fingerprint, machine_type, zone api incompatibility | min_cpu_platform undefined 143 | "google_compute_instance_group" => [:zone], # zone api incompatibility issue 144 | "google_compute_forwarding_rule" => %i{backend_service ip_version network region subnetwork}, # :backend_service, :ip_version, :network, :region, :subnetwork api incompatibility 145 | "google_compute_target_pool" => %i{backup_pool failover_ratio id region self_link}, # api incompatibility 146 | }.freeze 147 | 148 | GCP_TRANSLATED_RESOURCE_PROPERTIES = { 149 | }.freeze 150 | 151 | # readme content 152 | def self.readme; end 153 | 154 | # inspec.yml boilerplate content from 155 | # inspec/lib/plugins/inspec-init/templates/profiles/gcp/inspec.yml 156 | def self.inspec_yml 157 | yml = {} 158 | yml["inspec_version"] = ">= 2.3.5" 159 | yml["depends"] = [{ 160 | "name" => "inspec-gcp", 161 | "url" => "https://github.com/inspec/inspec-gcp/archive/master.tar.gz", 162 | }] 163 | yml["supports"] = [{ 164 | "platform" => "gcp", 165 | }] 166 | yml 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/inspec-iggy/plugin.rb: -------------------------------------------------------------------------------- 1 | require "inspec/plugin/v2" 2 | 3 | # The InspecPlugins namespace is where all plugins should declare themselves. 4 | # The 'Inspec' capitalization is used throughout the InSpec source code; yes, it's 5 | # strange. 6 | module InspecPlugins 7 | module Iggy 8 | class Plugin < ::Inspec.plugin(2) 9 | # Internal machine name of the plugin. InSpec will use this in errors, etc. 10 | plugin_name :'inspec-iggy' 11 | 12 | cli_command :terraform do 13 | # Calling this hook doesn't mean iggy is being executed - just that we 14 | # should be ready to do so. So, load the file that defines the functionality. 15 | # For example, InSpec will activate this hook when `inspec help` is 16 | # executed, so that this plugin's usage message will be included in the help. 17 | require "inspec-iggy/terraform/cli_command" 18 | 19 | # Having loaded our functionality, return a class that will let the 20 | # CLI engine tap into it. 21 | InspecPlugins::Iggy::Terraform::CliCommand 22 | end 23 | 24 | cli_command :cloudformation do 25 | require "inspec-iggy/cloudformation/cli_command" 26 | InspecPlugins::Iggy::CloudFormation::CliCommand 27 | end 28 | 29 | cli_command :iggy do 30 | require "inspec-iggy/iggy_cli_command" 31 | InspecPlugins::Iggy::CliCommand 32 | end 33 | 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/inspec-iggy/profile_helper.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # renders the profile from the parsed files 3 | 4 | require "yaml" 5 | 6 | require "inspec-iggy/platforms/aws_helper" 7 | require "inspec-iggy/platforms/azure_helper" 8 | require "inspec-iggy/platforms/gcp_helper" 9 | 10 | module InspecPlugins 11 | module Iggy 12 | class ProfileHelper 13 | # match the output of 'inspec init profile' 14 | # inspec/lib/plugins/inspec-init/lib/inspec-init/renderer.rb 15 | def self.render_profile(cli, options, source_file, controls, platform = nil) 16 | name = options[:name] 17 | overwrite_mode = options[:overwrite] 18 | 19 | # --------------------------- InSpec Code Generator --------------------------- 20 | cli.headline("InSpec Iggy Code Generator") 21 | 22 | full_destination_path = Pathname.new(Dir.pwd).join(name) 23 | 24 | if File.exist?(full_destination_path) && !overwrite_mode 25 | cli.plain_line "#{cli.emphasis(full_destination_path)} exists already, use --overwrite" 26 | cli.exit(1) 27 | end 28 | 29 | # ensure that full_destination_path directory is available 30 | FileUtils.mkdir_p(full_destination_path) 31 | 32 | # Creating new profile at /Users/mattray/ws/inspec-iggy/FOO 33 | cli.plain_line "Creating new profile at #{cli.emphasis(full_destination_path)}" 34 | # * Creating file README.md 35 | render_readme_md(cli, name, source_file, platform) 36 | # * Creating directory controls 37 | cli.list_item "Creating directory #{cli.emphasis("controls")}" 38 | FileUtils.mkdir_p("#{name}/controls") 39 | # * Creating file controls/generated.rb 40 | render_controls_rb(cli, name, controls) 41 | # * Creating file inspec.yml 42 | render_inspec_yml(cli, name, source_file, options, platform) 43 | cli.plain_line 44 | end 45 | 46 | def self.render_readme_md(cli, name, source_file, platform) 47 | cli.list_item "Creating file #{cli.emphasis("README.md")}" 48 | f = File.new("#{name}/README.md", "w") 49 | f.puts("# #{name}") 50 | f.puts 51 | f.puts("This profile was generated by InSpec-Iggy v#{Iggy::VERSION} from the #{source_file} source file.") 52 | 53 | f.puts(InspecPlugins::Iggy::Platforms::AwsHelper.readme) if platform.eql?("aws") 54 | f.puts(InspecPlugins::Iggy::Platforms::AzureHelper.readme) if platform.eql?("azure") 55 | f.puts(InspecPlugins::Iggy::Platforms::GcpHelper.readme) if platform.eql?("gcp") 56 | 57 | f.close 58 | end 59 | 60 | def self.render_inspec_yml(cli, name, source_file, options, platform) 61 | cli.list_item "Creating file #{cli.emphasis("inspec.yml")}" 62 | yml = {} 63 | yml["name"] = name 64 | yml["title"] = options[:title] 65 | yml["maintainer"] = options[:maintainer] 66 | yml["copyright"] = options[:copyright] 67 | yml["copyright_email"] = options[:email] 68 | yml["license"] = options[:license] 69 | yml["summary"] = options[:summary] 70 | yml["version"] = options[:version] 71 | yml["description"] = "Generated by InSpec-Iggy v#{Iggy::VERSION} from the #{source_file} source file." 72 | 73 | yml.merge!(InspecPlugins::Iggy::Platforms::AwsHelper.inspec_yml) if platform.eql?("aws") 74 | yml.merge!(InspecPlugins::Iggy::Platforms::AzureHelper.inspec_yml) if platform.eql?("azure") 75 | yml.merge!(InspecPlugins::Iggy::Platforms::GcpHelper.inspec_yml) if platform.eql?("gcp") 76 | 77 | f = File.new("#{name}/inspec.yml", "w") 78 | f.write(yml.to_yaml) 79 | f.close 80 | end 81 | 82 | def self.render_controls_rb(cli, name, controls) 83 | cli.list_item "Creating file #{cli.emphasis("controls/generated.rb")}" 84 | f = File.new("#{name}/controls/generated.rb", "w") 85 | f.write(controls) 86 | f.close 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/inspec-iggy/terraform/cli_command.rb: -------------------------------------------------------------------------------- 1 | # Terraform CLI command and options 2 | 3 | require "inspec/plugin/v2" 4 | 5 | require "inspec-iggy/version" 6 | require "inspec-iggy/profile_helper" 7 | require "inspec-iggy/terraform/generate" 8 | require "inspec-iggy/terraform/negative" 9 | 10 | module InspecPlugins::Iggy 11 | module Terraform 12 | class CliCommand < Inspec.plugin(2, :cli_command) 13 | subcommand_desc "terraform SUBCOMMAND ...", "Generate an InSpec profile from Terraform" 14 | 15 | class_option :debug, 16 | desc: "Verbose debugging messages", 17 | type: :boolean, 18 | default: false 19 | 20 | class_option :copyright, 21 | desc: "Name of the copyright holder", 22 | default: "The Authors" 23 | 24 | class_option :email, 25 | desc: "Email address of the author", 26 | default: "you@example.com" 27 | 28 | class_option :license, 29 | desc: "License for the profile", 30 | default: "Apache-2.0" 31 | 32 | class_option :maintainer, 33 | desc: "Name of the copyright holder", 34 | default: "The Authors" 35 | 36 | class_option :summary, 37 | desc: "One line summary for the profile", 38 | default: "An InSpec Compliance Profile" 39 | 40 | class_option :title, 41 | desc: "Human-readable name for the profile", 42 | default: "InSpec Profile" 43 | 44 | class_option :version, 45 | desc: "Specify the profile version", 46 | default: "0.1.0" 47 | 48 | class_option :overwrite, 49 | desc: "Overwrites existing profile directory", 50 | type: :boolean, 51 | default: false 52 | 53 | class_option :name, 54 | aliases: "-n", 55 | required: true, 56 | desc: "Name of profile to be generated" 57 | 58 | class_option :tfstate, 59 | aliases: "-t", 60 | desc: "Specify path to the input terraform.tfstate", 61 | default: "terraform.tfstate" 62 | 63 | class_option :platform, 64 | required: true, 65 | desc: "The InSpec platform providing the necessary resources (aws, azure, or gcp)" 66 | 67 | class_option :resourcepath, 68 | required: true, 69 | desc: "Specify path to the InSpec Resource Pack providing the necessary resources" 70 | 71 | desc "generate [options]", "Generate InSpec compliance controls from terraform.tfstate" 72 | def generate 73 | Inspec::Log.level = :debug if options[:debug] 74 | platform = options[:platform] 75 | resource_path = options[:resourcepath] 76 | # require validation that if platform or resourcepath are passed, both are available 77 | if platform || resource_path 78 | unless platform && resource_path 79 | error "You must pass both --platform and --resourcepath if using either" 80 | exit(1) 81 | end 82 | end 83 | generated_controls = InspecPlugins::Iggy::Terraform::Generate.parse_generate(options[:tfstate], resource_path, platform) 84 | printable_controls = InspecPlugins::Iggy::InspecHelper.tf_controls(options[:title], generated_controls, platform) 85 | InspecPlugins::Iggy::ProfileHelper.render_profile(ui, options, options[:tfstate], printable_controls, platform) 86 | exit 0 87 | end 88 | 89 | desc "negative [options]", "Generate negative InSpec compliance controls from terraform.tfstate" 90 | def negative 91 | Inspec::Log.level = :debug if options[:debug] 92 | platform = options[:platform] 93 | resource_path = options[:resourcepath] 94 | # require validation that if platform or resourcepath are passed, both are available 95 | if platform || resource_path 96 | unless platform && resource_path 97 | error "You must pass both --platform and --resourcepath if using either" 98 | exit(1) 99 | end 100 | end 101 | negative_controls = InspecPlugins::Iggy::Terraform::Negative.parse_negative(options[:tfstate], resource_path, platform) 102 | printable_controls = InspecPlugins::Iggy::InspecHelper.tf_controls(options[:title], negative_controls, platform) 103 | InspecPlugins::Iggy::ProfileHelper.render_profile(ui, options, options[:tfstate], printable_controls, platform) 104 | exit 0 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/inspec-iggy/terraform/generate.rb: -------------------------------------------------------------------------------- 1 | # parses Terraform d.tfstate files 2 | 3 | require "inspec/objects/control" 4 | require "inspec/objects/ruby_helper" 5 | require "inspec/objects/describe" 6 | 7 | require "inspec-iggy/file_helper" 8 | require "inspec-iggy/inspec_helper" 9 | 10 | module InspecPlugins::Iggy::Terraform 11 | class Generate 12 | # parse through the JSON and generate InSpec controls 13 | def self.parse_generate(tf_file, resource_path, platform) 14 | # parse the tfstate file to get the Terraform resources 15 | tfstate = InspecPlugins::Iggy::FileHelper.parse_json(tf_file) 16 | absolutename = File.absolute_path(tf_file) 17 | 18 | # take those Terraform resources and map to InSpec resources by name and keep all attributes 19 | # resources -> [{name1 -> {unfiltered_attributes}, name2 -> {unfiltered_attributes}] 20 | parsed_resources = parse_resources(tfstate, resource_path, platform) 21 | 22 | # InSpec controls generated from matched_resources and attributes 23 | generated_controls = parse_controls(parsed_resources, absolutename, platform) 24 | 25 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_generate generated_controls = #{generated_controls}" 26 | generated_controls 27 | end 28 | 29 | # returns the list of all InSpec resources found in the tfstate file 30 | def self.parse_resources(tfstate, resource_path, _platform) 31 | # iterate over the resources 32 | resources = {} 33 | tf_resources = tfstate["resources"] 34 | tf_resources.each do |tf_res| 35 | resource_type = tf_res["type"] 36 | next if resource_type.eql?("random_id") # this is a Terraform resource, not a provider resource 37 | 38 | # load resource pack resources 39 | InspecPlugins::Iggy::InspecHelper.load_resource_pack(resource_path) if resource_path 40 | 41 | # add translation layer 42 | if InspecPlugins::Iggy::InspecHelper::TRANSLATED_RESOURCES.key?(resource_type) 43 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_resources resource_type = #{resource_type} #{InspecPlugins::Iggy::InspecHelper::TRANSLATED_RESOURCES[resource_type]} TRANSLATED" 44 | resource_type = InspecPlugins::Iggy::InspecHelper::TRANSLATED_RESOURCES[resource_type] 45 | end 46 | resources[resource_type] = {} if resources[resource_type].nil? 47 | # does this match an InSpec resource? 48 | if InspecPlugins::Iggy::InspecHelper.available_resources.include?(resource_type) 49 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_resources resource_type = #{resource_type} MATCHED" 50 | tf_res["instances"].each do |instance| 51 | resource_id = instance["attributes"]["id"] 52 | resource_attributes = instance["attributes"] 53 | resources[resource_type][resource_id] = resource_attributes 54 | end 55 | else 56 | Inspec::Log.debug "Iggy::Terraform.Generate.parse_generate resource_type = #{resource_type} SKIPPED" 57 | end 58 | end 59 | resources 60 | end 61 | 62 | # take the resources and map to describes 63 | def self.parse_controls(resources, absolutename, platform) # rubocop:disable Metrics/AbcSize 64 | controls = [] 65 | # iterate over the resources types and their ids 66 | resources.keys.each do |resource_type| 67 | resources[resource_type].keys.each do |resource_id| 68 | # insert new control based off the resource's ID 69 | ctrl = Inspec::Control.new 70 | ctrl.id = "#{resource_type}::#{resource_id}" 71 | ctrl.title = "InSpec-Iggy #{resource_type}::#{resource_id}" 72 | ctrl.descriptions[:default] = "#{resource_type}::#{resource_id} from the source file #{absolutename}\nGenerated by InSpec-Iggy v#{InspecPlugins::Iggy::VERSION}" 73 | ctrl.impact = "1.0" 74 | 75 | describe = Inspec::Describe.new 76 | case platform # this may need to get refactored away once Azure is tested 77 | when "aws" 78 | qualifier = [resource_type, {}] 79 | if InspecPlugins::Iggy::InspecHelper.available_resource_qualifiers(platform).key?(resource_type) # there are additional qualifiers 80 | first = true 81 | InspecPlugins::Iggy::InspecHelper.available_resource_qualifiers(platform)[resource_type].each do |parameter| 82 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_controls #{resource_type} qualifier found = #{parameter} MATCHED" 83 | if first # this is the id for the resource 84 | value = resources[resource_type][resource_id]["id"] # pull value out of the tf attributes 85 | first = false 86 | else 87 | value = resources[resource_type][resource_id][parameter.to_s] # pull value out of the tf attributes 88 | end 89 | qualifier[1][parameter] = value 90 | end 91 | end 92 | describe.qualifier.push(qualifier) 93 | when "azure" # rubocop:disable Lint/EmptyWhen 94 | # this is a hack for azure, we need a better longterm solution 95 | # if resource.start_with?('azure_') 96 | # name = resource_id.split('/').last 97 | # else 98 | # name = resource_id 99 | # end 100 | 101 | # if resource_type.start_with?('azure_') 102 | # if resource_type.eql?('azure_resource_group') 103 | # describe.qualifier.push([resource_type, name: name]) 104 | # else 105 | # resource_group = resource_id.split('resourceGroups/').last.split('/').first 106 | # describe.qualifier.push([resource_type, name: name, group_name: resource_group]) 107 | # end 108 | when "gcp" 109 | qualifier = [resource_type, {}] 110 | if InspecPlugins::Iggy::InspecHelper.available_resource_qualifiers(platform).key?(resource_type) 111 | InspecPlugins::Iggy::InspecHelper.available_resource_qualifiers(platform)[resource_type].each do |parameter| 112 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_controls #{resource_type} qualifier found = #{parameter} MATCHED" 113 | value = resources[resource_type][resource_id][parameter.to_s] # pull value out of the tf attributes 114 | qualifier[1][parameter] = value 115 | end 116 | end 117 | describe.qualifier.push(qualifier) 118 | end 119 | 120 | # ensure the resource exists unless Azure, which currently doesn't support it as of InSpec 2.2 121 | describe.add_test(nil, "exist", nil) unless resource_type.start_with?("azure_") 122 | 123 | # if there's a match, see if there are matching InSpec properties 124 | inspec_properties = InspecPlugins::Iggy::InspecHelper.resource_properties(resource_type, platform) 125 | # push stuff back into inspec_properties? 126 | resources[resource_type][resource_id].keys.each do |attr| 127 | if inspec_properties.member?(attr) 128 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_controls #{resource_type} inspec_property = #{attr} MATCHED" 129 | value = resources[resource_type][resource_id][attr] 130 | if value 131 | # check to see if there is a translate for this attr 132 | property = InspecPlugins::Iggy::InspecHelper.translated_resource_property(platform, resource_type, attr) 133 | describe.add_test(property, "cmp", value) 134 | else 135 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_controls #{resource_type} inspec_property = #{attr} SKIPPED FOR NIL" 136 | end 137 | else 138 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_controls #{resource_type} inspec_property = #{attr} SKIPPED" 139 | end 140 | end 141 | 142 | ctrl.add_test(describe) 143 | controls.push(ctrl) 144 | end 145 | end 146 | Inspec::Log.debug "Iggy::Terraform::Generate.parse_generate controls = #{controls}" 147 | controls 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/inspec-iggy/terraform/negative.rb: -------------------------------------------------------------------------------- 1 | # returns negative of Terraform tfstate file coverage 2 | 3 | require "hashie" 4 | 5 | require "inspec/objects/control" 6 | require "inspec/objects/ruby_helper" 7 | require "inspec/objects/describe" 8 | 9 | require "inspec-iggy/file_helper" 10 | require "inspec-iggy/inspec_helper" 11 | require "inspec-iggy/terraform/generate" 12 | 13 | module InspecPlugins::Iggy::Terraform 14 | class Negative 15 | # parse through the JSON and generate InSpec controls 16 | def self.parse_negative(tf_file, resource_path, platform) 17 | tfstate = InspecPlugins::Iggy::FileHelper.parse_json(tf_file) 18 | sourcefile = File.absolute_path(tf_file) 19 | 20 | # take those Terraform resources and map to InSpec resources by name and keep all attributes 21 | parsed_resources = InspecPlugins::Iggy::Terraform::Generate.parse_resources(tfstate, resource_path, platform) 22 | 23 | # subtract matched resources from all available resources 24 | negative_controls = parse_unmatched_resources(parsed_resources, sourcefile, platform) 25 | negative_controls += parse_matched_resources(parsed_resources, sourcefile, platform) 26 | 27 | negative_controls 28 | end 29 | 30 | # return controls for the iterators of things unmatched in the terraform.tfstate 31 | def self.parse_unmatched_resources(resources, sourcefile, platform) 32 | resources.extend Hashie::Extensions::DeepFind # use to find iterators' values from other attributes 33 | unmatched_resources = InspecPlugins::Iggy::InspecHelper.available_resource_iterators(platform).keys - resources.keys 34 | Inspec::Log.debug "Terraform::Negative.parse_unmatched_resources unmatched_resources #{unmatched_resources}" 35 | unmatched_controls = [] 36 | unmatched_resources.each do |unmatched| 37 | unresources = InspecPlugins::Iggy::InspecHelper.available_resource_iterators(platform)[unmatched] 38 | iterator = unresources["iterator"] 39 | ctrl = Inspec::Control.new 40 | ctrl.id = "NEGATIVE-COVERAGE:#{iterator}" 41 | ctrl.title = "InSpec-Iggy NEGATIVE-COVERAGE:#{iterator}" 42 | ctrl.descriptions[:default] = "NEGATIVE-COVERAGE:#{iterator} from the source file #{sourcefile}\nGenerated by InSpec-Iggy v#{InspecPlugins::Iggy::VERSION}" 43 | ctrl.impact = "1.0" 44 | describe = Inspec::Describe.new 45 | qualifier = [iterator, {}] 46 | unresources["qualifiers"].each do |parameter| 47 | Inspec::Log.debug "Terraform::Negative.parse_unmatched_resources #{iterator} qualifier found = #{parameter} MATCHED" 48 | value = resources.deep_find(parameter.to_s) # value comes from another likely source. Assumption is values are consistent for this type of field 49 | qualifier[1][parameter] = value 50 | end 51 | describe.qualifier.push(qualifier) 52 | describe.add_test(nil, "exist", nil, { negated: true }) # last field is negated 53 | ctrl.add_test(describe) 54 | unmatched_controls.push(ctrl) 55 | end 56 | Inspec::Log.debug "Terraform::Negative.parse_unmatched_resources negative_controls = #{unmatched_controls}" 57 | unmatched_controls 58 | end 59 | 60 | # controls for iterators minus the matched resources 61 | def self.parse_matched_resources(resources, sourcefile, platform) # rubocop:disable Metrics/AbcSize 62 | Inspec::Log.debug "Terraform::Negative.parse_matched_resources matched_resources #{resources.keys}" 63 | matched_controls = [] 64 | resources.keys.each do |resource| 65 | resources[resource].extend Hashie::Extensions::DeepFind # use to find iterators' values from other attributes 66 | resource_iterators = InspecPlugins::Iggy::InspecHelper.available_resource_iterators(platform)[resource] 67 | if resource_iterators.nil? 68 | Inspec::Log.warn "No iterator matching #{resource} for #{platform} found!" 69 | next 70 | else 71 | iterator = resource_iterators["iterator"] 72 | index = resource_iterators["index"] 73 | Inspec::Log.debug "Terraform::Negative.parse_matched_resources iterator:#{iterator} index:#{index}" 74 | end 75 | # Nothing but the finest bespoke hand-built InSpec 76 | ctrl = "control 'NEGATIVE-COVERAGE:#{iterator}' do\n" 77 | ctrl += " title 'InSpec-Iggy NEGATIVE-COVERAGE:#{iterator}'\n" 78 | ctrl += " desc \"\n" 79 | ctrl += " NEGATIVE-COVERAGE:#{iterator} from the source file #{sourcefile}\n\n" 80 | ctrl += " Generated by InSpec-Iggy v#{InspecPlugins::Iggy::VERSION}\"\n\n" 81 | ctrl += " impact 1.0\n" 82 | # get the qualifiers for the resource iterator 83 | ctrl += " (#{iterator}.where({ " 84 | if resource_iterators["qualifiers"] 85 | resource_iterators["qualifiers"].each do |parameter| 86 | Inspec::Log.debug "Terraform::Negative.parse_matched_resources #{iterator} qualifier found = #{parameter} MATCHED" 87 | value = resources[resource].deep_find(parameter.to_s) # value comes from resources being evaluated. Assumption is values are consistent for this type of field 88 | unless value 89 | Inspec::Log.warn "Terraform::Negative.parse_matched_resources #{resource} no #{parameter} value found, searching outside scope." 90 | value = resources.deep_find(parameter.to_s) 91 | end 92 | ctrl += "#{parameter}: '#{value}', " 93 | end 94 | end 95 | ctrl += "}).#{index} - [\n" 96 | # iterate over the resources 97 | resources[resource].keys.each do |resource_name| 98 | ctrl += " '#{resource_name}',\n" 99 | end 100 | ctrl += " ]).each do |id|\n" 101 | ctrl += " describe #{resource}({ " 102 | # iterate over resource qualifiers 103 | first = true 104 | InspecPlugins::Iggy::InspecHelper.available_resource_qualifiers(platform)[resource].each do |parameter| 105 | if first # index is first 106 | ctrl += "#{parameter}: id, " 107 | first = false 108 | next 109 | end 110 | property = parameter.to_s 111 | properties = InspecPlugins::Iggy::InspecHelper.available_translated_resource_properties(platform, resource) 112 | if properties && properties.value?(parameter.to_s) 113 | property = properties.key(parameter.to_s) # translate back if necessary 114 | end 115 | # instead of looking up the key, find by value? 116 | Inspec::Log.debug "Iggy::Terraform::Negative.parse_matched_resources #{resource} qualifier found = #{property} MATCHED" 117 | value = resources[resource].deep_find(property) # value comes from resources being evaluated. Assumption is values are consistent for this type of field 118 | ctrl += "#{property}: '#{value}', " 119 | end 120 | ctrl += "}) do\n" 121 | ctrl += " it { should_not exist }\n" 122 | ctrl += " end\n" 123 | ctrl += " end\n" 124 | ctrl += "end\n\n" 125 | matched_controls.push(ctrl) 126 | end 127 | Inspec::Log.debug "Terraform::Negative.parse_matched_resources negative_controls = #{matched_controls}" 128 | matched_controls 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/inspec-iggy/version.rb: -------------------------------------------------------------------------------- 1 | # provide the version for the plugin 2 | 3 | module InspecPlugins 4 | module Iggy 5 | VERSION = "0.8.1".freeze 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/cloudformation/aws-4.5.4.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "AWSTemplateFormatVersion": "2010-09-09", 4 | "Description": "BJC Chef Demo (4.5.4)", 5 | "Parameters": { 6 | "AvailabilityZone": { 7 | "Description": "Availability Zone", 8 | "Type": "String", 9 | "Default": "us-west-2c" 10 | }, 11 | "DemoName": { 12 | "Description": "Name of the customer or organization", 13 | "Type": "String", 14 | "Default": "bjc-demo" 15 | }, 16 | "Version": { 17 | "Description": "Version", 18 | "Type": "String", 19 | "Default": "4.5.4" 20 | }, 21 | "KeyName": { 22 | "Description": "Name of an existing ec2 KeyPair to enable SSH access", 23 | "Type": "AWS::EC2::KeyPair::KeyName", 24 | "ConstraintDescription": "must be the name of an existing EC2 KeyPair." 25 | }, 26 | "SSHLocation": { 27 | "Description": "The IP address range that can be used to SSH to the EC2 instances", 28 | "Type": "String", 29 | "MinLength": "9", 30 | "MaxLength": "18", 31 | "Default": "0.0.0.0/0", 32 | "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 33 | "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x." 34 | }, 35 | "TTL": { 36 | "Description": "Time in hours for the demo to stay active. Default is 4, maximum is 720 hours (30 days).", 37 | "Type": "Number", 38 | "Default": 8, 39 | "MinValue": 0, 40 | "MaxValue": 720 41 | }, 42 | "ChefServerAMI": { 43 | "Type": "String", 44 | "Default": "ami-3e6f1a46", 45 | "Description": "AMI ID for the Chef Server" 46 | }, 47 | "BuildNode1AMI": { 48 | "Type": "String", 49 | "Default": "ami-cb6f1ab3", 50 | "Description": "AMI ID for Build Node 1" 51 | }, 52 | "BuildNode2AMI": { 53 | "Type": "String", 54 | "Default": "ami-3c6d1844", 55 | "Description": "AMI ID for Build Node 2" 56 | }, 57 | "BuildNode3AMI": { 58 | "Type": "String", 59 | "Default": "ami-4b6f1a33", 60 | "Description": "AMI ID for Build Node 3" 61 | }, 62 | "deliveredAMI": { 63 | "Type": "String", 64 | "Default": "ami-676f1a1f", 65 | "Description": "AMI ID for delivered" 66 | }, 67 | "ecomacceptanceAMI": { 68 | "Type": "String", 69 | "Default": "ami-de6c19a6", 70 | "Description": "AMI ID for ecomacceptance" 71 | }, 72 | "rehearsalAMI": { 73 | "Type": "String", 74 | "Default": "ami-846c19fc", 75 | "Description": "AMI ID for rehearsal" 76 | }, 77 | "unionAMI": { 78 | "Type": "String", 79 | "Default": "ami-1f621767", 80 | "Description": "AMI ID for union" 81 | }, 82 | "WindowsWorkstation1AMI": { 83 | "Type": "String", 84 | "Default": "ami-ba6e1bc2", 85 | "Description": "AMI ID for the Windows Workstation" 86 | }, 87 | "AutomateAMI": { 88 | "Type": "String", 89 | "Default": "ami-7d6c1905", 90 | "Description": "AMI ID for the Automate Server" 91 | } 92 | }, 93 | "Resources": { 94 | "InstanceProfile" : { 95 | "Type" : "AWS::IAM::InstanceProfile", 96 | "Properties" : { 97 | "Path" : "/", 98 | "Roles" : ["chefDemo"] 99 | } 100 | }, 101 | "VPC": { 102 | "Type": "AWS::EC2::VPC", 103 | "Properties": { 104 | "CidrBlock": "172.31.0.0/16", 105 | "EnableDnsSupport": "true", 106 | "EnableDnsHostnames": "true", 107 | "Tags": [ 108 | { 109 | "Key": "Application", 110 | "Value": { 111 | "Ref": "AWS::StackId" 112 | }, 113 | "Key": "Name", 114 | "Value": { 115 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "VPC" ] ] 116 | } 117 | } 118 | ] 119 | } 120 | }, 121 | "SubnetAutomate": { 122 | "Type": "AWS::EC2::Subnet", 123 | "Properties": { 124 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 125 | "VpcId": { 126 | "Ref": "VPC" 127 | }, 128 | "CidrBlock": "172.31.54.0/24", 129 | "Tags": [ 130 | { 131 | "Key": "Application", 132 | "Value": { 133 | "Ref": "AWS::StackId" 134 | }, 135 | "Key": "Name", 136 | "Value": { 137 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Automate Subnet" ] ] 138 | } 139 | } 140 | ] 141 | } 142 | }, 143 | "SubnetProd": { 144 | "Type": "AWS::EC2::Subnet", 145 | "Properties": { 146 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 147 | "VpcId": { 148 | "Ref": "VPC" 149 | }, 150 | "CidrBlock": "172.31.62.0/24", 151 | "Tags": [ 152 | { 153 | "Key": "Application", 154 | "Value": { 155 | "Ref": "AWS::StackId" 156 | }, 157 | "Key": "Name", 158 | "Value": { 159 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Prod Subnet" ] ] 160 | } 161 | } 162 | ] 163 | } 164 | }, 165 | "SubnetWorkstations": { 166 | "Type": "AWS::EC2::Subnet", 167 | "Properties": { 168 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 169 | "VpcId": { 170 | "Ref": "VPC" 171 | }, 172 | "CidrBlock": "172.31.10.0/24", 173 | "Tags": [ 174 | { 175 | "Key": "Application", 176 | "Value": { 177 | "Ref": "AWS::StackId" 178 | }, 179 | "Key": "Name", 180 | "Value": { 181 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Workstations Subnet" ] ] 182 | } 183 | } 184 | ] 185 | } 186 | }, 187 | "InternetGateway": { 188 | "Type": "AWS::EC2::InternetGateway", 189 | "Properties": { 190 | "Tags": [ 191 | { 192 | "Key": "Application", 193 | "Value": { 194 | "Ref": "AWS::StackId" 195 | }, 196 | "Key": "Name", 197 | "Value": { 198 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, " IG" ] ] 199 | } 200 | } 201 | ] 202 | } 203 | }, 204 | "AttachGateway": { 205 | "Type": "AWS::EC2::VPCGatewayAttachment", 206 | "Properties": { 207 | "VpcId": { 208 | "Ref": "VPC" 209 | }, 210 | "InternetGatewayId": { 211 | "Ref": "InternetGateway" 212 | } 213 | } 214 | }, 215 | "RouteTable": { 216 | "Type": "AWS::EC2::RouteTable", 217 | "Properties": { 218 | "VpcId": { 219 | "Ref": "VPC" 220 | }, 221 | "Tags": [ 222 | { 223 | "Key": "Application", 224 | "Value": { 225 | "Ref": "AWS::StackId" 226 | }, 227 | "Key": "Name", 228 | "Value": { 229 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Demo RouteTable" ] ] 230 | } 231 | } 232 | ] 233 | } 234 | }, 235 | "Route": { 236 | "Type": "AWS::EC2::Route", 237 | "DependsOn": "AttachGateway", 238 | "Properties": { 239 | "RouteTableId": { 240 | "Ref": "RouteTable" 241 | }, 242 | "DestinationCidrBlock": "0.0.0.0/0", 243 | "GatewayId": { 244 | "Ref": "InternetGateway" 245 | } 246 | } 247 | }, 248 | "SubnetRouteTableAssociationAutomate": { 249 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 250 | "Properties": { 251 | "SubnetId": { 252 | "Ref": "SubnetAutomate" 253 | }, 254 | "RouteTableId": { 255 | "Ref": "RouteTable" 256 | } 257 | } 258 | }, 259 | "SubnetRouteTableAssociationProd": { 260 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 261 | "Properties": { 262 | "SubnetId": { 263 | "Ref": "SubnetProd" 264 | }, 265 | "RouteTableId": { 266 | "Ref": "RouteTable" 267 | } 268 | } 269 | }, 270 | "SubnetRouteTableAssociationWorkstations": { 271 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 272 | "Properties": { 273 | "SubnetId": { 274 | "Ref": "SubnetWorkstations" 275 | }, 276 | "RouteTableId": { 277 | "Ref": "RouteTable" 278 | } 279 | } 280 | }, 281 | "NetworkAcl": { 282 | "Type": "AWS::EC2::NetworkAcl", 283 | "Properties": { 284 | "VpcId": { 285 | "Ref": "VPC" 286 | }, 287 | "Tags": [ 288 | { 289 | "Key": "Application", 290 | "Value": { 291 | "Ref": "AWS::StackId" 292 | }, 293 | "Key": "Name", 294 | "Value": { 295 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "NetworkAcl" ] ] 296 | } 297 | } 298 | ] 299 | } 300 | }, 301 | "InboundNetworkAclEntry": { 302 | "Type": "AWS::EC2::NetworkAclEntry", 303 | "Properties": { 304 | "NetworkAclId": { 305 | "Ref": "NetworkAcl" 306 | }, 307 | "RuleNumber": "100", 308 | "Protocol": "-1", 309 | "RuleAction": "allow", 310 | "Egress": "false", 311 | "CidrBlock": "0.0.0.0/0" 312 | } 313 | }, 314 | "OutBoundNetworkAclEntry": { 315 | "Type": "AWS::EC2::NetworkAclEntry", 316 | "Properties": { 317 | "NetworkAclId": { 318 | "Ref": "NetworkAcl" 319 | }, 320 | "RuleNumber": "100", 321 | "Protocol": "-1", 322 | "RuleAction": "allow", 323 | "Egress": "true", 324 | "CidrBlock": "0.0.0.0/0" 325 | } 326 | }, 327 | "SubnetNetworkAclAssociationAutomate": { 328 | "Type": "AWS::EC2::SubnetNetworkAclAssociation", 329 | "Properties": { 330 | "SubnetId": { 331 | "Ref": "SubnetAutomate" 332 | }, 333 | "NetworkAclId": { 334 | "Ref": "NetworkAcl" 335 | } 336 | } 337 | }, 338 | "SubnetNetworkAclAssociationProd": { 339 | "Type": "AWS::EC2::SubnetNetworkAclAssociation", 340 | "Properties": { 341 | "SubnetId": { 342 | "Ref": "SubnetProd" 343 | }, 344 | "NetworkAclId": { 345 | "Ref": "NetworkAcl" 346 | } 347 | } 348 | }, 349 | "SubnetNetworkAclAssociationPOCWorkstations": { 350 | "Type": "AWS::EC2::SubnetNetworkAclAssociation", 351 | "Properties": { 352 | "SubnetId": { 353 | "Ref": "SubnetWorkstations" 354 | }, 355 | "NetworkAclId": { 356 | "Ref": "NetworkAcl" 357 | } 358 | } 359 | }, 360 | "WindowsWorkstation1": { 361 | "Type": "AWS::EC2::Instance", 362 | "Properties": { 363 | "InstanceType": "c4.large", 364 | "EbsOptimized" : "true", 365 | "IamInstanceProfile" : {"Ref" : "InstanceProfile"}, 366 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 367 | "NetworkInterfaces": [ 368 | { 369 | "GroupSet": [ 370 | { 371 | "Ref": "DemoSecurityGroup" 372 | } 373 | ], 374 | "AssociatePublicIpAddress": "true", 375 | "PrivateIpAddress": "172.31.54.201", 376 | "DeviceIndex": "0", 377 | "DeleteOnTermination": "true", 378 | "SubnetId": { 379 | "Ref": "SubnetAutomate" 380 | } 381 | } 382 | ], 383 | "KeyName": { 384 | "Ref": "KeyName" 385 | }, 386 | "UserData" : { 387 | "Fn::Base64" : { 388 | "Fn::Join" : [ 389 | "", 390 | ["\n", 391 | "set-executionpolicy -executionpolicy unrestricted -force -scope LocalMachine", 392 | "" 393 | ] 394 | ] 395 | } 396 | }, 397 | "ImageId": { 398 | "Ref": "WindowsWorkstation1AMI" 399 | }, 400 | "Tags": [ 401 | { 402 | "Key": "Name", 403 | "Value": { 404 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Workstation" ] ] 405 | } 406 | } 407 | ] 408 | } 409 | }, 410 | "BuildNode1": { 411 | "Type": "AWS::EC2::Instance", 412 | "Properties": { 413 | "InstanceType": "m4.large", 414 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 415 | "NetworkInterfaces": [ 416 | { 417 | "GroupSet": [ 418 | { 419 | "Ref": "DemoSecurityGroup" 420 | } 421 | ], 422 | "AssociatePublicIpAddress": "true", 423 | "PrivateIpAddress": "172.31.54.51", 424 | "DeviceIndex": "0", 425 | "DeleteOnTermination": "true", 426 | "SubnetId": { 427 | "Ref": "SubnetAutomate" 428 | } 429 | } 430 | ], 431 | "KeyName": { "Ref": "KeyName" }, 432 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 433 | "#!/bin/bash -xe\n", 434 | "hostnamectl set-hostname build-node-1\n", 435 | "sleep 90\n", 436 | "sudo chef-client\n"]]} 437 | }, 438 | "ImageId": { 439 | "Ref": "BuildNode1AMI" 440 | }, 441 | "Tags": [ 442 | { 443 | "Key": "Name", 444 | "Value": { 445 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Build Node 1" ] ] 446 | } 447 | } 448 | ] 449 | } 450 | }, 451 | "BuildNode2": { 452 | "Type": "AWS::EC2::Instance", 453 | "Properties": { 454 | "InstanceType": "m4.large", 455 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 456 | "NetworkInterfaces": [ 457 | { 458 | "GroupSet": [ 459 | { 460 | "Ref": "DemoSecurityGroup" 461 | } 462 | ], 463 | "AssociatePublicIpAddress": "true", 464 | "PrivateIpAddress": "172.31.54.52", 465 | "DeviceIndex": "0", 466 | "DeleteOnTermination": "true", 467 | "SubnetId": { 468 | "Ref": "SubnetAutomate" 469 | } 470 | } 471 | ], 472 | "KeyName": { "Ref": "KeyName" }, 473 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 474 | "#!/bin/bash -xe\n", 475 | "hostnamectl set-hostname build-node-2\n", 476 | "sleep 90\n", 477 | "sudo chef-client\n"]]} 478 | }, 479 | "ImageId": { 480 | "Ref": "BuildNode2AMI" 481 | }, 482 | "Tags": [ 483 | { 484 | "Key": "Name", 485 | "Value": { 486 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Build Node 2" ] ] 487 | } 488 | } 489 | ] 490 | } 491 | }, 492 | "BuildNode3": { 493 | "Type": "AWS::EC2::Instance", 494 | "Properties": { 495 | "InstanceType": "m4.large", 496 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 497 | "NetworkInterfaces": [ 498 | { 499 | "GroupSet": [ 500 | { 501 | "Ref": "DemoSecurityGroup" 502 | } 503 | ], 504 | "AssociatePublicIpAddress": "true", 505 | "PrivateIpAddress": "172.31.54.53", 506 | "DeviceIndex": "0", 507 | "DeleteOnTermination": "true", 508 | "SubnetId": { 509 | "Ref": "SubnetAutomate" 510 | } 511 | } 512 | ], 513 | "KeyName": { "Ref": "KeyName" }, 514 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 515 | "#!/bin/bash -xe\n", 516 | "hostnamectl set-hostname build-node-3\n", 517 | "sleep 90\n", 518 | "sudo chef-client\n"]]} 519 | }, 520 | "ImageId": { 521 | "Ref": "BuildNode3AMI" 522 | }, 523 | "Tags": [ 524 | { 525 | "Key": "Name", 526 | "Value": { 527 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Build Node 3" ] ] 528 | } 529 | } 530 | ] 531 | } 532 | }, 533 | "delivered": { 534 | "Type": "AWS::EC2::Instance", 535 | "Properties": { 536 | "InstanceType": "m4.large", 537 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 538 | "NetworkInterfaces": [ 539 | { 540 | "GroupSet": [ 541 | { 542 | "Ref": "DemoSecurityGroup" 543 | } 544 | ], 545 | "AssociatePublicIpAddress": "true", 546 | "PrivateIpAddress": "172.31.54.101", 547 | "DeviceIndex": "0", 548 | "DeleteOnTermination": "true", 549 | "SubnetId": { 550 | "Ref": "SubnetAutomate" 551 | } 552 | } 553 | ], 554 | "KeyName": { "Ref": "KeyName" }, 555 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 556 | "#!/bin/bash -xe\n", 557 | "hostnamectl set-hostname delivered\n", 558 | "sleep 90\n", 559 | "sudo chef-client\n" 560 | ]]} 561 | }, 562 | "ImageId": { 563 | "Ref": "deliveredAMI" 564 | }, 565 | "Tags": [ 566 | { 567 | "Key": "Name", 568 | "Value": { 569 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "delivered" ] ] 570 | } 571 | } 572 | ] 573 | } 574 | }, 575 | "ecomacceptance": { 576 | "Type": "AWS::EC2::Instance", 577 | "Properties": { 578 | "InstanceType": "m4.large", 579 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 580 | "NetworkInterfaces": [ 581 | { 582 | "GroupSet": [ 583 | { 584 | "Ref": "DemoSecurityGroup" 585 | } 586 | ], 587 | "AssociatePublicIpAddress": "true", 588 | "PrivateIpAddress": "172.31.54.102", 589 | "DeviceIndex": "0", 590 | "DeleteOnTermination": "true", 591 | "SubnetId": { 592 | "Ref": "SubnetAutomate" 593 | } 594 | } 595 | ], 596 | "KeyName": { "Ref": "KeyName" }, 597 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 598 | "#!/bin/bash -xe\n", 599 | "hostnamectl set-hostname ecomacceptance\n", 600 | "sleep 90\n", 601 | "sudo chef-client\n" 602 | ]]} 603 | }, 604 | "ImageId": { 605 | "Ref": "ecomacceptanceAMI" 606 | }, 607 | "Tags": [ 608 | { 609 | "Key": "Name", 610 | "Value": { 611 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "ecomacceptance" ] ] 612 | } 613 | } 614 | ] 615 | } 616 | }, 617 | "rehearsal": { 618 | "Type": "AWS::EC2::Instance", 619 | "Properties": { 620 | "InstanceType": "m4.large", 621 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 622 | "NetworkInterfaces": [ 623 | { 624 | "GroupSet": [ 625 | { 626 | "Ref": "DemoSecurityGroup" 627 | } 628 | ], 629 | "AssociatePublicIpAddress": "true", 630 | "PrivateIpAddress": "172.31.54.103", 631 | "DeviceIndex": "0", 632 | "DeleteOnTermination": "true", 633 | "SubnetId": { 634 | "Ref": "SubnetAutomate" 635 | } 636 | } 637 | ], 638 | "KeyName": { "Ref": "KeyName" }, 639 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 640 | "#!/bin/bash -xe\n", 641 | "hostnamectl set-hostname rehearsal\n", 642 | "sleep 90\n", 643 | "sudo chef-client\n" 644 | ]]} 645 | }, 646 | "ImageId": { 647 | "Ref": "rehearsalAMI" 648 | }, 649 | "Tags": [ 650 | { 651 | "Key": "Name", 652 | "Value": { 653 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "rehearsal" ] ] 654 | } 655 | } 656 | ] 657 | } 658 | }, 659 | "union": { 660 | "Type": "AWS::EC2::Instance", 661 | "Properties": { 662 | "InstanceType": "m4.large", 663 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 664 | "NetworkInterfaces": [ 665 | { 666 | "GroupSet": [ 667 | { 668 | "Ref": "DemoSecurityGroup" 669 | } 670 | ], 671 | "AssociatePublicIpAddress": "true", 672 | "PrivateIpAddress": "172.31.54.104", 673 | "DeviceIndex": "0", 674 | "DeleteOnTermination": "true", 675 | "SubnetId": { 676 | "Ref": "SubnetAutomate" 677 | } 678 | } 679 | ], 680 | "KeyName": { "Ref": "KeyName" }, 681 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 682 | "#!/bin/bash -xe\n", 683 | "hostnamectl set-hostname union\n", 684 | "sleep 90\n", 685 | "sudo chef-client\n" 686 | ]]} 687 | }, 688 | "ImageId": { 689 | "Ref": "unionAMI" 690 | }, 691 | "Tags": [ 692 | { 693 | "Key": "Name", 694 | "Value": { 695 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "union" ] ] 696 | } 697 | } 698 | ] 699 | } 700 | }, 701 | "Chef": { 702 | "Type": "AWS::EC2::Instance", 703 | "Properties": { 704 | "InstanceType": "c4.xlarge", 705 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 706 | "BlockDeviceMappings" : [ 707 | { 708 | "DeviceName" : "/dev/sda1", 709 | "Ebs" : { "VolumeSize" : "50" } 710 | } 711 | ], 712 | "NetworkInterfaces": [ 713 | { 714 | "GroupSet": [ 715 | { 716 | "Ref": "DemoSecurityGroup" 717 | } 718 | ], 719 | "AssociatePublicIpAddress": "true", 720 | "PrivateIpAddress": "172.31.54.10", 721 | "DeviceIndex": "0", 722 | "DeleteOnTermination": "true", 723 | "SubnetId": { 724 | "Ref": "SubnetAutomate" 725 | } 726 | } 727 | ], 728 | "KeyName": { "Ref": "KeyName" }, 729 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 730 | "#!/bin/bash -xe\n", 731 | "hostnamectl set-hostname chef\n", 732 | "chef-server-ctl reconfigure\n"]]} 733 | }, 734 | "ImageId": { 735 | "Ref": "ChefServerAMI" 736 | }, 737 | "Tags": [ 738 | { 739 | "Key": "Name", 740 | "Value": { 741 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Chef Server" ] ] 742 | } 743 | } 744 | ] 745 | } 746 | }, 747 | "Automate": { 748 | "Type": "AWS::EC2::Instance", 749 | "Properties": { 750 | "InstanceType": "c4.xlarge", 751 | "AvailabilityZone": { "Ref": "AvailabilityZone" }, 752 | "BlockDeviceMappings" : [ 753 | { 754 | "DeviceName" : "/dev/sda1", 755 | "Ebs" : { "VolumeSize" : "50" } 756 | } 757 | ] , 758 | "NetworkInterfaces": [ 759 | { 760 | "GroupSet": [ 761 | { 762 | "Ref": "DemoSecurityGroup" 763 | } 764 | ], 765 | "AssociatePublicIpAddress": "true", 766 | "PrivateIpAddress": "172.31.54.11", 767 | "DeviceIndex": "0", 768 | "DeleteOnTermination": "true", 769 | "SubnetId": { 770 | "Ref": "SubnetAutomate" 771 | } 772 | } 773 | ], 774 | "KeyName": { "Ref": "KeyName" }, 775 | "UserData": { "Fn::Base64" : { "Fn::Join" : ["", [ 776 | "#!/bin/bash -xe\n", 777 | "hostnamectl set-hostname automate\n", 778 | "delivery-ctl reconfigure\n"]]} 779 | }, 780 | "ImageId": { 781 | "Ref": "AutomateAMI" 782 | }, 783 | "Tags": [ 784 | { 785 | "Key": "Name", 786 | "Value": { 787 | "Fn::Join" : [ " ", [ { "Ref": "DemoName" }, "Automate Server" ] ] 788 | } 789 | } 790 | ] 791 | } 792 | }, 793 | "DemoSecurityGroup": { 794 | "Type": "AWS::EC2::SecurityGroup", 795 | "Properties": { 796 | "VpcId": { 797 | "Ref": "VPC" 798 | }, 799 | "GroupDescription": "Enable required ports for Chef Server", 800 | "SecurityGroupIngress": [ 801 | { 802 | "IpProtocol": "tcp", 803 | "FromPort": "22", 804 | "ToPort": "22", 805 | "CidrIp": { 806 | "Ref": "SSHLocation" 807 | } 808 | }, 809 | { 810 | "IpProtocol": "tcp", 811 | "FromPort": "0", 812 | "ToPort": "65535", 813 | "CidrIp": "172.31.0.0/16" 814 | }, 815 | { 816 | "IpProtocol": "tcp", 817 | "FromPort": "3389", 818 | "ToPort": "3389", 819 | "CidrIp": "0.0.0.0/0" 820 | }, 821 | { 822 | "IpProtocol": "tcp", 823 | "FromPort": "443", 824 | "ToPort": "443", 825 | "CidrIp": "0.0.0.0/0" 826 | }, 827 | { 828 | "IpProtocol": "icmp", 829 | "FromPort": "8", 830 | "ToPort": "-1", 831 | "CidrIp": "0.0.0.0/0" 832 | }, 833 | { 834 | "IpProtocol": "udp", 835 | "FromPort": "3389", 836 | "ToPort": "3389", 837 | "CidrIp": "0.0.0.0/0" 838 | }, 839 | { 840 | "IpProtocol": "tcp", 841 | "FromPort": "5985", 842 | "ToPort": "5985", 843 | "CidrIp": "0.0.0.0/0" 844 | } 845 | ] 846 | } 847 | } 848 | }, 849 | "Outputs": 850 | {"WindowsWorkstation1PubDNS":{"Description":"Public IP address of the Windows Workstation","Value":{"Fn::GetAtt":["WindowsWorkstation1","PublicIp"]}}} 851 | } 852 | -------------------------------------------------------------------------------- /test/fixtures/cloudformation/bad.json: -------------------------------------------------------------------------------- 1 | { 2 | this 3 | file 4 | is 5 | junk 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/terraform/configs/main.tf: -------------------------------------------------------------------------------- 1 | # Two-Tier example from https://github.com/terraform-providers/terraform-provider-aws 2 | 3 | # Specify the provider and access details 4 | provider "aws" { 5 | region = "${var.aws_region}" 6 | } 7 | 8 | # Create a VPC to launch our instances into 9 | resource "aws_vpc" "default" { 10 | cidr_block = "10.0.0.0/16" 11 | 12 | tags { 13 | iggy_name_hong_kong = "hong-kong", 14 | iggy_url_hong_kong = "https://github.com/mattray/hong-kong-compliance" 15 | } 16 | } 17 | 18 | # Create an internet gateway to give our subnet access to the outside world 19 | resource "aws_internet_gateway" "default" { 20 | vpc_id = "${aws_vpc.default.id}" 21 | } 22 | 23 | # Grant the VPC internet access on its main route table 24 | resource "aws_route" "internet_access" { 25 | route_table_id = "${aws_vpc.default.main_route_table_id}" 26 | destination_cidr_block = "0.0.0.0/0" 27 | gateway_id = "${aws_internet_gateway.default.id}" 28 | } 29 | 30 | # Create a subnet to launch our instances into 31 | resource "aws_subnet" "default" { 32 | vpc_id = "${aws_vpc.default.id}" 33 | cidr_block = "10.0.1.0/24" 34 | map_public_ip_on_launch = true 35 | } 36 | 37 | # A security group for the ELB so it is accessible via the web 38 | resource "aws_security_group" "elb" { 39 | name = "terraform_example_elb" 40 | description = "Used in the terraform" 41 | vpc_id = "${aws_vpc.default.id}" 42 | 43 | # HTTP access from anywhere 44 | ingress { 45 | from_port = 80 46 | to_port = 80 47 | protocol = "tcp" 48 | cidr_blocks = ["0.0.0.0/0"] 49 | } 50 | 51 | # outbound internet access 52 | egress { 53 | from_port = 0 54 | to_port = 0 55 | protocol = "-1" 56 | cidr_blocks = ["0.0.0.0/0"] 57 | } 58 | } 59 | 60 | # Our default security group to access 61 | # the instances over SSH and HTTP 62 | resource "aws_security_group" "default" { 63 | name = "terraform_example" 64 | description = "Used in the terraform" 65 | vpc_id = "${aws_vpc.default.id}" 66 | 67 | # SSH access from anywhere 68 | ingress { 69 | from_port = 22 70 | to_port = 22 71 | protocol = "tcp" 72 | cidr_blocks = ["0.0.0.0/0"] 73 | } 74 | 75 | # HTTP access from the VPC 76 | ingress { 77 | from_port = 80 78 | to_port = 80 79 | protocol = "tcp" 80 | cidr_blocks = ["10.0.0.0/16"] 81 | } 82 | 83 | # outbound internet access 84 | egress { 85 | from_port = 0 86 | to_port = 0 87 | protocol = "-1" 88 | cidr_blocks = ["0.0.0.0/0"] 89 | } 90 | } 91 | 92 | resource "aws_elb" "web" { 93 | name = "terraform-example-elb" 94 | 95 | subnets = ["${aws_subnet.default.id}"] 96 | security_groups = ["${aws_security_group.elb.id}"] 97 | instances = ["${aws_instance.web.id}"] 98 | 99 | listener { 100 | instance_port = 80 101 | instance_protocol = "http" 102 | lb_port = 80 103 | lb_protocol = "http" 104 | } 105 | } 106 | 107 | resource "aws_key_pair" "auth" { 108 | key_name = "${var.key_name}" 109 | public_key = "${file(var.public_key_path)}" 110 | } 111 | 112 | resource "aws_instance" "web" { 113 | # The connection block tells our provisioner how to 114 | # communicate with the resource (instance) 115 | connection { 116 | # The default username for our AMI 117 | user = "ubuntu" 118 | 119 | # The connection will use the local SSH agent for authentication. 120 | private_key = "${file(var.private_key_path)}" 121 | } 122 | 123 | instance_type = "t2.micro" 124 | 125 | # Lookup the correct AMI based on the region 126 | # we specified 127 | ami = "${lookup(var.aws_amis, var.aws_region)}" 128 | 129 | # The name of our SSH keypair we created above. 130 | key_name = "${aws_key_pair.auth.id}" 131 | 132 | # Our Security group to allow HTTP and SSH access 133 | vpc_security_group_ids = ["${aws_security_group.default.id}"] 134 | 135 | # We're going to launch into the same subnet as our ELB. In a production 136 | # environment it's more common to have a separate private subnet for 137 | # backend instances. 138 | subnet_id = "${aws_subnet.default.id}" 139 | 140 | # We run a remote provisioner on the instance after creating it. 141 | # In this case, we just install nginx and start it. By default, 142 | # this should be on port 80 143 | provisioner "remote-exec" { 144 | inline = [ 145 | "sudo apt-get -y update", 146 | "sudo apt-get -y install apache2", 147 | ] 148 | } 149 | 150 | tags { 151 | iggy_name_apache_baseline = "apache-baseline", 152 | iggy_url_apache_baseline = "https://github.com/dev-sec/apache-baseline", 153 | iggy_name_linux_baseline = "linux-baseline", 154 | iggy_url_linux_baseline = "https://github.com/dev-sec/linux-baseline" 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /test/fixtures/terraform/configs/outputs.tf: -------------------------------------------------------------------------------- 1 | output "address" { 2 | value = "${aws_elb.web.dns_name}" 3 | } 4 | 5 | output "instance_id" { 6 | value = "${aws_instance.web.id}" 7 | } 8 | 9 | output "vpc_id" { 10 | value = "${aws_vpc.default.id}" 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/terraform/configs/variables.tf: -------------------------------------------------------------------------------- 1 | # Two-Tier example from https://github.com/terraform-providers/terraform-provider-aws 2 | 3 | variable "public_key_path" { 4 | description = < e 42 | iggy_run_result.payload.check_json_error = e 43 | end 44 | iggy_run_result.payload.check_result = check_result 45 | 46 | # Now run inspec json, which translates a profile into JSON 47 | export_cmd = "json " 48 | # export_cmd += File.join(tmp_dir, 'iggy-test-profile') 49 | export_cmd += "iggy-test-profile" 50 | 51 | export_result = run_inspec_process(export_cmd) 52 | begin 53 | iggy_run_result.payload.export_json = JSON.parse(export_result.stdout) 54 | rescue JSON::ParserError => e 55 | iggy_run_result.payload.export_json_error = e 56 | end 57 | iggy_run_result.payload.export_result = export_result 58 | end 59 | end 60 | 61 | end 62 | -------------------------------------------------------------------------------- /test/inspec/README.md: -------------------------------------------------------------------------------- 1 | # InSpec Profile for testing InSpec-Iggy behavior 2 | 3 | This InSpec profile tests the InSpec-Iggy plugin for regressions 4 | -------------------------------------------------------------------------------- /test/inspec/controls/inspec.rb: -------------------------------------------------------------------------------- 1 | control "inspec CLI" do 2 | describe command("bundle exec inspec") do 3 | its("stdout") { should match(/inspec iggy/) } 4 | its("stdout") { should match(/inspec terraform SUBCOMMAND .../) } 5 | its("stdout") { should match(/inspec cloudformation SUBCOMMAND .../) } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/inspec/controls/inspec_cloudformation.rb: -------------------------------------------------------------------------------- 1 | control "inspec cloudformation" do 2 | describe command("bundle exec inspec cloudformation") do 3 | its("stdout") { should match(/inspec cloudformation generate \[options\] -n, --name=NAME -s/) } 4 | its("stdout") { should match(/inspec cloudformation help \[COMMAND\]/) } 5 | its("stdout") { should match(/\[--log-level=LOG_LEVEL\]/) } 6 | its("stdout") { should match(/\[--log-location=LOG_LOCATION\]/) } 7 | # its('stdout') { should match (/\[--debug\], \[--no-debug\]/) } 8 | # its('stdout') { should match (/\[--copyright=COPYRIGHT\]/) } 9 | # its('stdout') { should match (/\[--email=EMAIL\]/) } 10 | # its('stdout') { should match (/\[--license=LICENSE\]/) } 11 | # its('stdout') { should match (/\[--maintainer=MAINTAINER\]/) } 12 | # its('stdout') { should match (/\[--summary=SUMMARY\]/) } 13 | # its('stdout') { should match (/\[--title=TITLE\]/) } 14 | # its('stdout') { should match (/\[--version=VERSION\]/) } 15 | # its('stdout') { should match (/\[--overwrite\], \[--no-overwrite\]/) } 16 | # its('stdout') { should match (/-n, --name=NAME/) } 17 | # its('stdout') { should match (/-s, \[--stack=STACK\]/) } 18 | # its('stdout') { should match (/-t, \[--template=TEMPLATE\]/) } 19 | end 20 | end 21 | 22 | control "inspec cloudformation help generate" do 23 | describe command("bundle exec inspec cloudformation help generate") do 24 | its("stdout") { should match(/inspec cloudformation generate \[options\] -n, --name=NAME -s/) } 25 | its("stdout") { should match(/\[--debug\], \[--no-debug\]/) } 26 | its("stdout") { should match(/\[--copyright=COPYRIGHT\]/) } 27 | its("stdout") { should match(/\[--email=EMAIL\]/) } 28 | its("stdout") { should match(/\[--license=LICENSE\]/) } 29 | its("stdout") { should match(/\[--maintainer=MAINTAINER\]/) } 30 | its("stdout") { should match(/\[--summary=SUMMARY\]/) } 31 | its("stdout") { should match(/\[--title=TITLE\]/) } 32 | its("stdout") { should match(/\[--version=VERSION\]/) } 33 | its("stdout") { should match(/\[--overwrite\], \[--no-overwrite\]/) } 34 | its("stdout") { should match(/-n, --name=NAME/) } 35 | its("stdout") { should match(/-s, --stack=STACK/) } 36 | its("stdout") { should match(/-t, --template=TEMPLATE/) } 37 | its("stdout") { should match(/\[--log-level=LOG_LEVEL\]/) } 38 | its("stdout") { should match(/\[--log-location=LOG_LOCATION\]/) } 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/inspec/controls/inspec_iggy.rb: -------------------------------------------------------------------------------- 1 | control "inspec iggy" do 2 | describe command("bundle exec inspec iggy") do 3 | its("stdout") { should match(/inspec iggy help \[COMMAND\]/) } 4 | its("stdout") { should match(/inspec iggy version/) } 5 | end 6 | end 7 | 8 | control "inspec iggy version" do 9 | describe command("bundle exec inspec iggy version") do 10 | its("stdout") { should match(/Iggy v0.8.1/) } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/inspec/controls/inspec_terraform.rb: -------------------------------------------------------------------------------- 1 | control "inspec terraform" do 2 | describe command("bundle exec inspec terraform") do 3 | its("stdout") { should match(/inspec terraform generate \[options\]/) } 4 | its("stdout") { should match(/inspec terraform help \[COMMAND\]/) } 5 | its("stdout") { should match(/inspec terraform negative \[options\]/) } 6 | its("stdout") { should match(/\[--log-level=LOG_LEVEL\]/) } 7 | its("stdout") { should match(/\[--log-location=LOG_LOCATION\]/) } 8 | its("stdout") { should match(/\[--debug\], \[--no-debug\]/) } 9 | its("stdout") { should match(/\[--copyright=COPYRIGHT\]/) } 10 | its("stdout") { should match(/\[--email=EMAIL\]/) } 11 | its("stdout") { should match(/\[--license=LICENSE\]/) } 12 | its("stdout") { should match(/\[--maintainer=MAINTAINER\]/) } 13 | its("stdout") { should match(/\[--summary=SUMMARY\]/) } 14 | its("stdout") { should match(/\[--title=TITLE\]/) } 15 | its("stdout") { should match(/\[--version=VERSION\]/) } 16 | its("stdout") { should match(/\[--overwrite\], \[--no-overwrite\]/) } 17 | its("stdout") { should match(/-n, --name=NAME/) } 18 | its("stdout") { should match(/-t, \[--tfstate=TFSTATE\]/) } 19 | its("stdout") { should match(/--platform=PLATFORM/) } 20 | its("stdout") { should match(/--resourcepath=RESOURCEPATH/) } 21 | end 22 | 23 | describe command("bundle exec inspec terraform generate") do 24 | its("stderr") { should match(/No value provided for required options '--name', '--platform', '--resourcepath'/) } 25 | end 26 | 27 | describe command("bundle exec inspec terraform negative") do 28 | its("stderr") { should match(/No value provided for required options '--name', '--platform', '--resourcepath'/) } 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/inspec/controls/inspec_terraform_aws.rb: -------------------------------------------------------------------------------- 1 | tmp_dir = input("tmp_dir") 2 | resource_dir = input("resource_dir") 3 | 4 | control "inspec terraform generate --name AWS-terraform-two-tier" do 5 | 6 | describe directory tmp_dir do 7 | it { should exist } 8 | end 9 | 10 | describe directory resource_dir do 11 | it { should exist } 12 | end 13 | 14 | describe command("bundle exec inspec terraform generate --name #{tmp_dir}/AWS-terraform-two-tier -t test/fixtures/terraform/tfstates/aws-terraform-two-tier-example.tfstate --platform aws --resourcepath #{resource_dir} --title AWS-TERRAFORM-TWO-TIER --maintainer 'Matt Ray' --copyright nobody --email inspec-iggy@mattray.dev --summary 'AWS-terraform-two-tier testing profile' --version 1.2.3") do 15 | its("exit_status") { should cmp 0 } 16 | its("stdout") { should match(/InSpec Iggy Code Generator/) } # skip the non ASCII characters 17 | its("stdout") { should match(/Creating new profile at/) } 18 | its("stdout") { should match(/Creating file/) } 19 | its("stdout") { should match(/Creating directory/) } 20 | end 21 | 22 | describe directory "#{tmp_dir}/AWS-terraform-two-tier/" do 23 | it { should exist } 24 | end 25 | 26 | describe directory "#{tmp_dir}/AWS-terraform-two-tier/controls/" do 27 | it { should exist } 28 | end 29 | 30 | describe file "#{tmp_dir}/AWS-terraform-two-tier/inspec.yml" do 31 | its("content") { should match(/AWS-terraform-two-tier"$/) } 32 | its("content") { should match(/title: AWS-TERRAFORM-TWO-TIER$/) } 33 | its("content") { should match(/maintainer: Matt Ray$/) } 34 | its("content") { should match(/copyright: nobody$/) } 35 | its("content") { should match(/copyright_email: inspec-iggy@mattray.dev$/) } 36 | its("content") { should match(/license: Apache-2.0$/) } 37 | its("content") { should match(/summary: AWS-terraform-two-tier testing profile$/) } 38 | its("content") { should match(/version: 1.2.3$/) } 39 | its("content") { should match(%r{description: Generated by InSpec-Iggy v0.8.1 from the test/fixtures/terraform/tfstates/aws-terraform-two-tier-example.tfstate$}) } 40 | its("content") { should match(/inspec_version: "~> 4"$/) } 41 | its("content") { should match(/depends:$/) } 42 | its("content") { should match(/- name: inspec-aws$/) } 43 | its("content") { should match(%r{url: https://github.com/inspec/inspec-aws/archive/master.tar.gz$}) } 44 | its("content") { should match(/supports:$/) } 45 | its("content") { should match(/- platform: aws$/) } 46 | end 47 | 48 | describe file "#{tmp_dir}/AWS-terraform-two-tier/README.md" do 49 | its("content") { should match(/#*AWS-terraform-two-tier$/) } 50 | its("content") { should match(%r{^This profile was generated by InSpec-Iggy v0.8.1 from the test/fixtures/terraform/tfstates/aws-terraform-two-tier-example.tfstate source file.$}) } 51 | end 52 | 53 | describe file "#{tmp_dir}/AWS-terraform-two-tier/controls/generated.rb" do 54 | its("content") { should match(/aws_ec2_instance::i-05c6d20469a0a0ee9 from the source file/) } 55 | its("content") { should match(/aws_security_group::sg-01199abc1619d7613 from the source file/) } 56 | its("content") { should match(/aws_security_group::sg-0eac4a147658285b7 from the source file/) } 57 | its("content") { should match(/aws_subnet::subnet-00f265d40d3d0a227 from the source file/) } 58 | its("content") { should match(/aws_vpc::vpc-035d7e339ce59ad62 from the source file/) } 59 | its("content") { should match(%r{test/fixtures/terraform/tfstates/aws-terraform-two-tier-example.tfstate$}) } 60 | its("content") { should match(/control "aws_ec2_instance::i-05c6d20469a0a0ee9" do$/) } 61 | its("content") { should match(/control "aws_elb::terraform-example-elb" do$/) } 62 | its("content") { should match(/control "aws_security_group::sg-01199abc1619d7613" do$/) } 63 | its("content") { should match(/control "aws_security_group::sg-0eac4a147658285b7" do$/) } 64 | its("content") { should match(/control "aws_subnet::subnet-00f265d40d3d0a227" do$/) } 65 | its("content") { should match(/control "aws_vpc::vpc-035d7e339ce59ad62" do$/) } 66 | its("content") { should match(/describe aws_ec2_instance\({:instance_id=>"i-05c6d20469a0a0ee9"}\) do$/) } 67 | its("content") { should match(/describe aws_elb\({:load_balancer_name=>"terraform-example-elb"}\) do$/) } 68 | its("content") { should match(/describe aws_security_group\({:group_id=>"sg-01199abc1619d7613", :vpc_id=>"vpc-035d7e339ce59ad62"}\) do$/) } 69 | its("content") { should match(/describe aws_security_group\({:group_id=>"sg-0eac4a147658285b7", :vpc_id=>"vpc-035d7e339ce59ad62"}\) do$/) } 70 | its("content") { should match(/describe aws_subnet\({:subnet_id=>"subnet-00f265d40d3d0a227"}\) do$/) } 71 | its("content") { should match(/describe aws_vpc\({:vpc_id=>"vpc-035d7e339ce59ad62"}\) do$/) } 72 | its("content") { should match(/it { should exist }$/) } 73 | its("content") { should match(/its\("availability_zone"\) { should cmp "us-west-2c" }$/) } 74 | its("content") { should match(/its\("availability_zones"\) { should cmp \["us-west-2c"\] }$/) } 75 | its("content") { should match(%r{its\("cidr_block"\) { should cmp "10.0.0.0/16" }$}) } 76 | its("content") { should match(%r{its\("cidr_block"\) { should cmp "10.0.1.0/24" }$}) } 77 | its("content") { should match(/its\("description"\) { should cmp "Used in the terraform" }$/) } 78 | its("content") { should match(/its\("dhcp_options_id"\) { should cmp "dopt-8d3211eb" }$/) } 79 | its("content") { should match(/its\("dns_name"\) { should cmp "terraform-example-elb-2051343015.us-west-2.elb.amazonaws.com" }$/) } 80 | its("content") { should match(/its\("group_name"\) { should cmp "terraform_example" }$/) } 81 | its("content") { should match(/its\("group_name"\) { should cmp "terraform_example_elb" }$/) } 82 | its("content") { should match(/its\("instance_tenancy"\) { should cmp "default" }$/) } 83 | its("content") { should match(/its\("load_balancer_name"\) { should cmp "terraform-example-elb" }$/) } 84 | its("content") { should match(/its\("owner_id"\) { should cmp "112758395563" }$/) } 85 | its("content") { should match(/its\("subnet_id"\) { should cmp "subnet-00f265d40d3d0a227" }$/) } 86 | its("content") { should match(/its\("subnets"\) { should cmp \["subnet-00f265d40d3d0a227"\] }$/) } 87 | its("content") { should match(/its\("vpc_id"\) { should cmp "vpc-035d7e339ce59ad62" }$/) } 88 | its("content") { should match(/title "AWS-TERRAFORM-TWO-TIER: generated by Iggy v0.8.1"$/) } 89 | its("content") { should match(/title "InSpec-Iggy aws_ec2_instance::i-05c6d20469a0a0ee9"$/) } 90 | its("content") { should match(/title "InSpec-Iggy aws_elb::terraform-example-elb"$/) } 91 | its("content") { should match(/title "InSpec-Iggy aws_security_group::sg-01199abc1619d7613"$/) } 92 | its("content") { should match(/title "InSpec-Iggy aws_security_group::sg-0eac4a147658285b7"$/) } 93 | its("content") { should match(/title "InSpec-Iggy aws_subnet::subnet-00f265d40d3d0a227"$/) } 94 | its("content") { should match(/title "InSpec-Iggy aws_vpc::vpc-035d7e339ce59ad62"$/) } 95 | end 96 | 97 | describe command("bundle exec inspec terraform generate --name #{tmp_dir}/AWS-terraform-elb-example -t test/fixtures/terraform/tfstates/aws-terraform-elb-example.tfstate --platform aws --resourcepath #{resource_dir} --title AWS-TERRAFORM-ELB-EXAMPLE --maintainer 'Matt Ray' --copyright nobody --email inspec-iggy@mattray.dev --summary 'AWS-terraform-elb-example testing profile' --version 1.2.3") do 98 | its("exit_status") { should cmp 0 } 99 | its("stdout") { should match(/InSpec Iggy Code Generator/) } # skip the non ASCII characters 100 | its("stdout") { should match(/Creating new profile at/) } 101 | its("stdout") { should match(/Creating file/) } 102 | its("stdout") { should match(/Creating directory/) } 103 | end 104 | 105 | describe directory "#{tmp_dir}/AWS-terraform-elb-example/" do 106 | it { should exist } 107 | end 108 | 109 | describe directory "#{tmp_dir}/AWS-terraform-elb-example/controls/" do 110 | it { should exist } 111 | end 112 | 113 | describe file "#{tmp_dir}/AWS-terraform-elb-example/inspec.yml" do 114 | its("content") { should match(/AWS-terraform-elb-example"$/) } 115 | its("content") { should match(/title: AWS-TERRAFORM-ELB-EXAMPLE$/) } 116 | its("content") { should match(/maintainer: Matt Ray$/) } 117 | its("content") { should match(/copyright: nobody$/) } 118 | its("content") { should match(/copyright_email: inspec-iggy@mattray.dev$/) } 119 | its("content") { should match(/license: Apache-2.0$/) } 120 | its("content") { should match(/summary: AWS-terraform-elb-example testing profile$/) } 121 | its("content") { should match(/version: 1.2.3$/) } 122 | its("content") { should match(%r{description: Generated by InSpec-Iggy v0.8.1 from the test/fixtures/terraform/tfstates/aws-terraform-elb-example.tfstate$}) } 123 | its("content") { should match(/inspec_version: "~> 4"$/) } 124 | its("content") { should match(/depends:$/) } 125 | its("content") { should match(/- name: inspec-aws$/) } 126 | its("content") { should match(%r{url: https://github.com/inspec/inspec-aws/archive/master.tar.gz$}) } 127 | its("content") { should match(/supports:$/) } 128 | its("content") { should match(/- platform: aws$/) } 129 | end 130 | 131 | describe file "#{tmp_dir}/AWS-terraform-elb-example/README.md" do 132 | its("content") { should match(/#*AWS-terraform-elb-example$/) } 133 | its("content") { should match(%r{^This profile was generated by InSpec-Iggy v0.8.1 from the test/fixtures/terraform/tfstates/aws-terraform-elb-example.tfstate source file.$}) } 134 | end 135 | 136 | describe file "#{tmp_dir}/AWS-terraform-elb-example/controls/generated.rb" do 137 | its("content") { should match(/Generated by InSpec-Iggy v0.8.1$/) } 138 | its("content") { should match(%r{/test/fixtures/terraform/tfstates/aws-terraform-elb-example.tfstate$}) } 139 | its("content") { should match(/aws_ec2_instance::i-0093ad1c115857458 from the source file/) } 140 | its("content") { should match(/aws_elb::example-elb from the source file/) } 141 | its("content") { should match(/aws_route_table::rtb-0beaa5171b8c1a961 from the source file/) } 142 | its("content") { should match(/aws_security_group::sg-071f16066bbf117eb from the source file/) } 143 | its("content") { should match(/aws_security_group::sg-076d9eeaf5f60b04e from the source file/) } 144 | its("content") { should match(/aws_subnet::subnet-00c91dee0349a24ea from the source file/) } 145 | its("content") { should match(/aws_vpc::vpc-09b99a40b26aa93a1 from the source file/) } 146 | its("content") { should match(/control "aws_ec2_instance::i-0093ad1c115857458" do$/) } 147 | its("content") { should match(/control "aws_elb::example-elb" do$/) } 148 | its("content") { should match(/control "aws_route_table::rtb-0beaa5171b8c1a961" do$/) } 149 | its("content") { should match(/control "aws_security_group::sg-071f16066bbf117eb" do$/) } 150 | its("content") { should match(/control "aws_security_group::sg-076d9eeaf5f60b04e" do$/) } 151 | its("content") { should match(/control "aws_subnet::subnet-00c91dee0349a24ea" do$/) } 152 | its("content") { should match(/control "aws_vpc::vpc-09b99a40b26aa93a1" do$/) } 153 | its("content") { should match(/describe aws_ec2_instance\({:instance_id=>"i-0093ad1c115857458"}\) do$/) } 154 | its("content") { should match(/describe aws_elb\({:load_balancer_name=>"example-elb"}\) do$/) } 155 | its("content") { should match(/describe aws_route_table\({:route_table_id=>"rtb-0beaa5171b8c1a961"}\) do$/) } 156 | its("content") { should match(/describe aws_security_group\({:group_id=>"sg-071f16066bbf117eb", :vpc_id=>"vpc-09b99a40b26aa93a1"}\) do$/) } 157 | its("content") { should match(/describe aws_security_group\({:group_id=>"sg-076d9eeaf5f60b04e", :vpc_id=>"vpc-09b99a40b26aa93a1"}\) do$/) } 158 | its("content") { should match(/describe aws_subnet\({:subnet_id=>"subnet-00c91dee0349a24ea"}\) do$/) } 159 | its("content") { should match(/describe aws_vpc\({:vpc_id=>"vpc-09b99a40b26aa93a1"}\) do$/) } 160 | its("content") { should match(/impact 1.0$/) } 161 | its("content") { should match(/it { should exist }$/) } 162 | its("content") { should match(/its\("availability_zone"\) { should cmp "us-west-2c" }$/) } 163 | its("content") { should match(/its\("availability_zones"\) { should cmp \["us-west-2c"\] }$/) } 164 | its("content") { should match(%r{its\("cidr_block"\) { should cmp "10.0.0.0/16" }$}) } 165 | its("content") { should match(%r{its\("cidr_block"\) { should cmp "10.0.0.0/24" }$}) } 166 | its("content") { should match(/its\("description"\) { should cmp "Used in the terraform" }$/) } 167 | its("content") { should match(/its\("dhcp_options_id"\) { should cmp "dopt-8d3211eb" }$/) } 168 | its("content") { should match(/its\("dns_name"\) { should cmp "example-elb-99716389.us-west-2.elb.amazonaws.com" }$/) } 169 | its("content") { should match(/its\("group_name"\) { should cmp "elb_sg" }$/) } 170 | its("content") { should match(/its\("group_name"\) { should cmp "instance_sg" }$/) } 171 | its("content") { should match(/its\("instance_tenancy"\) { should cmp "default" }$/) } 172 | its("content") { should match(/its\("load_balancer_name"\) { should cmp "example-elb" }$/) } 173 | its("content") { should match(/its\("owner_id"\) { should cmp "112758395563" }$/) } 174 | its("content") { should match(/its\("propagating_vgws"\) { should cmp \[\] }$/) } 175 | its("content") { should match(/its\("subnet_id"\) { should cmp "subnet-00c91dee0349a24ea" }$/) } 176 | its("content") { should match(/its\("subnets"\) { should cmp \["subnet-00c91dee0349a24ea"\] }$/) } 177 | its("content") { should match(/its\("vpc_id"\) { should cmp "vpc-09b99a40b26aa93a1" }$/) } 178 | its("content") { should match(/title "AWS-TERRAFORM-ELB-EXAMPLE: generated by Iggy v0.8.1"$/) } 179 | its("content") { should match(/title "InSpec-Iggy aws_ec2_instance::i-0093ad1c115857458"$/) } 180 | its("content") { should match(/title "InSpec-Iggy aws_elb::example-elb"$/) } 181 | its("content") { should match(/title "InSpec-Iggy aws_route_table::rtb-0beaa5171b8c1a961"$/) } 182 | its("content") { should match(/title "InSpec-Iggy aws_security_group::sg-071f16066bbf117eb"$/) } 183 | its("content") { should match(/title "InSpec-Iggy aws_security_group::sg-076d9eeaf5f60b04e"$/) } 184 | its("content") { should match(/title "InSpec-Iggy aws_subnet::subnet-00c91dee0349a24ea"$/) } 185 | its("content") { should match(/title "InSpec-Iggy aws_vpc::vpc-09b99a40b26aa93a1"$/) } 186 | end 187 | 188 | end 189 | -------------------------------------------------------------------------------- /test/inspec/inspec.yml: -------------------------------------------------------------------------------- 1 | name: inspec-iggy 2 | title: InSpec Profile for testing InSpec-Iggy 3 | maintainer: Matt Ray 4 | copyright: Matt Ray 5 | copyright_email: matt@chef.io 6 | license: Apache-2.0 7 | summary: An InSpec Compliance Profile for testing InSpec-Iggy 8 | version: 0.1.0 9 | supports: 10 | platform: os 11 | -------------------------------------------------------------------------------- /test/integration/resource_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # 3 | # Author:: Matt Ray () 4 | # 5 | # Copyright:: 2018, Chef Software, Inc 6 | # 7 | require "helper" 8 | 9 | require "inspec-iggy/inspec_helper" 10 | 11 | module IggyUnitTests 12 | class InSpecResources < Minitest::Test 13 | 14 | def known_resources 15 | { 16 | # List some resources we expect to heve 17 | # name => an expected property 18 | "aws_vpc" => "cidr_block", 19 | "directory" => "owner", 20 | "package" => "version", 21 | } 22 | end 23 | 24 | def test_it_should_list_resources 25 | known_resources.each_key do |resource_name| 26 | assert_includes(InspecPlugins::Iggy::InspecHelper::RESOURCES, resource_name) 27 | end 28 | end 29 | 30 | def test_it_should_know_resource_properties 31 | known_resources.each do |resource_name, property| 32 | assert_includes(InspecPlugins::Iggy::InspecHelper.resource_properties(resource_name), property) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/unit/cloudformation_cli_args_spec.rb: -------------------------------------------------------------------------------- 1 | # This unit test performs some tests to verify that the command line options for 2 | # inspec-iggy are correct. 3 | 4 | require "minitest/autorun" 5 | 6 | # Load the class under test, the CliCommand definition. 7 | require "inspec-iggy/cloudformation/cli_command" 8 | 9 | # In general, plugin authors can choose many different test harnesses, such as RSpec or Minitest/Spec. 10 | # However, Iggy loads all of InSpec, which causes interference with both of those, so here we use 11 | # minitest-assertion style. 12 | module IggyUnitTests 13 | module CfLets 14 | def cli_class 15 | InspecPlugins::Iggy::CloudFormation::CliCommand 16 | end 17 | 18 | def commands 19 | %w{ 20 | generate 21 | help 22 | } 23 | end 24 | end 25 | 26 | class CloudFormationCli 27 | class CommandSet < Minitest::Test 28 | include CfLets 29 | 30 | def test_it_should_have_the_right_number_of_commands 31 | assert_equal(commands.count, cli_class.all_commands.count) 32 | end 33 | 34 | def test_it_should_have_the_right_commands 35 | commands.each do |command| 36 | assert_includes(cli_class.all_commands.keys, command) 37 | end 38 | end 39 | end 40 | 41 | class GenerateCommand < Minitest::Test 42 | include CfLets 43 | 44 | def all_options 45 | %i{ 46 | copyright 47 | debug 48 | email 49 | license 50 | maintainer 51 | name 52 | overwrite 53 | stack 54 | summary 55 | template 56 | title 57 | version 58 | } 59 | end 60 | 61 | def no_default_options 62 | %i{ 63 | name 64 | stack 65 | template 66 | } 67 | end 68 | 69 | def short_options 70 | { 71 | name: ["-n"], 72 | stack: ["-s"], 73 | template: ["-t"], 74 | } 75 | end 76 | 77 | def boolean_options 78 | %i{ 79 | debug 80 | overwrite 81 | } 82 | end 83 | 84 | # This is a Hash of Structs that tells us details of options for the 'core' subcommand. 85 | def generate_options 86 | cli_class.all_commands["generate"].options 87 | end 88 | 89 | def test_it_should_have_the_right_option_count 90 | assert_equal(all_options.count, generate_options.count) 91 | end 92 | 93 | def test_it_should_have_the_right_options 94 | assert_equal(all_options.sort, generate_options.keys.sort) 95 | end 96 | 97 | def test_it_should_have_descriptions_for_all_options 98 | all_options.each do |option| 99 | refute_nil(generate_options[option].description) 100 | end 101 | end 102 | 103 | def test_it_should_have_a_default_for_most_options 104 | (all_options - no_default_options).each do |option| 105 | refute_nil(generate_options[option].default, option) 106 | end 107 | 108 | no_default_options.each do |option| 109 | assert(generate_options[option].required) 110 | end 111 | end 112 | 113 | def test_it_should_have_certain_options_be_typed_boolean 114 | boolean_options.each do |option| 115 | assert_equal(:boolean, generate_options[option].type) 116 | end 117 | end 118 | 119 | def test_it_should_have_some_options_be_abbreviated 120 | short_options.each do |option, abbrevs| 121 | assert_equal(abbrevs.sort, generate_options[option].aliases.sort) 122 | end 123 | end 124 | 125 | # Argument count 126 | # The 'generate' command does not accept arguments. 127 | def test_it_should_take_no_arguments 128 | assert_equal(0, cli_class.instance_method(:generate).arity) 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /test/unit/plugin_def_spec.rb: -------------------------------------------------------------------------------- 1 | # This unit test performs some tests to verify that 2 | # the inspec-iggy plugin is configured correctly. 3 | 4 | require "minitest/autorun" 5 | 6 | # Load the class under test, the Plugin definition. 7 | require "inspec-iggy/plugin" 8 | 9 | # In general, plugin authors can choose many different test harnesses, such as RSpec or Minitest/Spec. 10 | # However, Iggy loads all of InSpec, which causes interference with both of those, so here we use 11 | # minitest-assertion style. 12 | module IggyUnitTests 13 | class Plugin < Minitest::Test 14 | def setup 15 | # Internally, plugins are always known by a Symbol name. Convert here. 16 | @plugin_name = :'inspec-iggy' 17 | # The Registry knows about all plugins that ship with InSpec by 18 | # default, as well as any that are installed by the user. When a 19 | # plugin definition is loaded, it will also self-register. 20 | @registry = Inspec::Plugin::V2::Registry.instance 21 | # # The plugin status record tells us what the Registry knows. 22 | @status = @registry[@plugin_name] 23 | end 24 | 25 | # Does the Registry know about us at all? 26 | def test_it_should_be_registered 27 | assert(@registry.known_plugin?(@plugin_name)) 28 | end 29 | 30 | # The plugin system formerly had an undocumented v1 API; 31 | # this should be a real v2 plugin. 32 | def test_it_should_be_an_api_v2_plugin 33 | assert_equal(2, @status.api_generation) 34 | end 35 | 36 | # Plugins can support several different activator hooks, each of which has a type. 37 | # Since this is a CliCommand plugin, we'd expect to see that among our types. 38 | def test_it_should_include_a_cli_command_activator_hook 39 | assert_includes(@status.plugin_types, :cli_command) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/unit/terraform_cli_args_spec.rb: -------------------------------------------------------------------------------- 1 | # This unit test performs some tests to verify that the command line options for 2 | # inspec-iggy are correct. 3 | 4 | require "minitest/autorun" 5 | 6 | # Load the class under test, the CliCommand definition. 7 | require "inspec-iggy/terraform/cli_command" 8 | 9 | # In general, plugin authors can choose many different test harnesses, such as RSpec or Minitest/Spec. 10 | # However, Iggy loads all of InSpec, which causes interference with both of those, so here we use 11 | # minitest-assertion style. 12 | module IggyUnitTests 13 | module TfLets 14 | def cli_class 15 | InspecPlugins::Iggy::Terraform::CliCommand 16 | end 17 | 18 | def commands 19 | %w{ 20 | generate 21 | help 22 | negative 23 | } 24 | end 25 | end 26 | 27 | class TerraformCli 28 | # This is the CLI Command implementation class. 29 | # It is a subclass of Thor, which is a CLI framework. 30 | # This unit test file is mostly about verifying the Thor settings. 31 | 32 | class CommandSet < Minitest::Test 33 | include TfLets 34 | 35 | def test_it_should_have_the_right_number_of_commands 36 | assert_equal(commands.count, cli_class.all_commands.count) 37 | end 38 | 39 | def test_it_should_have_the_right_commands 40 | commands.each do |command| 41 | assert_includes(cli_class.all_commands.keys, command) 42 | end 43 | end 44 | end 45 | 46 | class GenerateCommand < Minitest::Test 47 | include TfLets 48 | 49 | def all_options 50 | %i{ 51 | copyright 52 | debug 53 | email 54 | license 55 | log_level 56 | log_location 57 | maintainer 58 | name 59 | overwrite 60 | platform 61 | resourcepath 62 | summary 63 | tfstate 64 | title 65 | version 66 | } 67 | end 68 | 69 | def no_default_options 70 | %i{ 71 | log_level 72 | log_location 73 | name 74 | platform 75 | resourcepath 76 | } 77 | end 78 | 79 | def not_required_options 80 | [ 81 | :log_level, 82 | :log_location, 83 | :platform, 84 | :resourcepath, # AWS is out-of-the-box? 85 | ] 86 | end 87 | 88 | def short_options 89 | { 90 | name: ["-n"], 91 | tfstate: ["-t"], 92 | } 93 | end 94 | 95 | def boolean_options 96 | %i{ 97 | debug 98 | overwrite 99 | } 100 | end 101 | 102 | # This is a Hash of Structs that tells us details of options for the 'core' subcommand. 103 | def cli_options 104 | cli_class.class_options 105 | end 106 | 107 | def test_generate_should_have_the_right_option_count 108 | assert_equal(all_options.count, cli_options.count) 109 | end 110 | 111 | def test_generate_should_have_the_right_options 112 | assert_equal(all_options.sort, cli_options.keys.sort) 113 | end 114 | 115 | def test_generate_should_have_descriptions_for_all_options 116 | all_options.each do |option| 117 | refute_nil(cli_options[option].description) 118 | end 119 | end 120 | 121 | def test_generate_should_have_a_default_for_most_options 122 | (all_options - no_default_options).each do |option| 123 | refute_nil(cli_options[option].default) 124 | end 125 | 126 | (no_default_options - not_required_options).each do |option| 127 | assert(cli_options[option].required) 128 | end 129 | end 130 | 131 | def test_generate_should_have_certain_options_be_typed_boolean 132 | boolean_options.each do |option| 133 | assert_equal(:boolean, cli_options[option].type) 134 | end 135 | end 136 | 137 | def test_generate_should_have_some_options_be_abbreviated 138 | short_options.each do |option, abbrevs| 139 | assert_equal(abbrevs.sort, cli_options[option].aliases.sort) 140 | end 141 | end 142 | 143 | # Argument count 144 | # The 'generate' command does not accept arguments. 145 | def test_generate_should_take_no_arguments 146 | assert_equal(0, cli_class.instance_method(:generate).arity) 147 | end 148 | 149 | # 'inspec terraform negative' currently has all the same options as 'generate' 150 | def test_negative_should_have_the_right_option_count 151 | assert_equal(all_options.count, cli_options.count) 152 | end 153 | 154 | def test_negative_should_have_the_right_options 155 | assert_equal(all_options.sort, cli_options.keys.sort) 156 | end 157 | 158 | def test_negative_should_have_descriptions_for_all_options 159 | all_options.each do |option| 160 | refute_nil(cli_options[option].description) 161 | end 162 | end 163 | 164 | def test_negative_should_have_a_default_for_most_options 165 | (all_options - no_default_options).each do |option| 166 | refute_nil(cli_options[option].default) 167 | end 168 | 169 | (no_default_options - not_required_options).each do |option| 170 | assert(cli_options[option].required) 171 | end 172 | end 173 | 174 | def test_negative_should_have_certain_options_be_typed_boolean 175 | boolean_options.each do |option| 176 | assert_equal(:boolean, cli_options[option].type) 177 | end 178 | end 179 | 180 | def test_negative_should_have_some_options_be_abbreviated 181 | short_options.each do |option, abbrevs| 182 | assert_equal(abbrevs.sort, cli_options[option].aliases.sort) 183 | end 184 | end 185 | 186 | # Argument count 187 | # The 'negative' command does not accept arguments. 188 | def test_negative_should_take_no_arguments 189 | assert_equal(0, cli_class.instance_method(:negative).arity) 190 | end 191 | end 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /test/unit/version_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # 3 | # Author:: Matt Ray () 4 | # 5 | # Copyright:: 2018, Chef Software, Inc 6 | # 7 | 8 | require "minitest/autorun" 9 | require "inspec-iggy/version" 10 | 11 | module IggyUnitTests 12 | class Version < Minitest::Test 13 | def test_should_have_a_version_constant_defined 14 | assert_kind_of(String, InspecPlugins::Iggy::VERSION) 15 | end 16 | end 17 | end 18 | --------------------------------------------------------------------------------