├── .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 | [](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 |
--------------------------------------------------------------------------------