inspec-iggy 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +15 -0
- data/README.md +114 -0
- data/inspec-iggy.gemspec +26 -0
- data/lib/inspec-iggy.rb +7 -0
- data/lib/inspec-iggy/cli.rb +53 -0
- data/lib/inspec-iggy/cloudformation.rb +104 -0
- data/lib/inspec-iggy/inspec_helper.rb +59 -0
- data/lib/inspec-iggy/terraform.rb +146 -0
- data/lib/inspec-iggy/version.rb +11 -0
- data/test/bad.json +6 -0
- data/test/bjc-demo-aws-4.5.4.json +851 -0
- data/test/main.tf +156 -0
- data/test/outputs.tf +11 -0
- data/test/terraform.tfstate +383 -0
- data/test/variables.tf +35 -0
- metadata +80 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3dc2db85eac33aa51e03baf7f0df30858a7f1815
|
4
|
+
data.tar.gz: e44f8c04a3998030335b6a2dcf2ff225c7b18d4f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9c8086684b141e0590b9a55f1b135922a4c9f4f7df3c3c32ec6422c890c7fe24a61443b16eac2e35bedb02f3a031a7efc7586a81d378addcd4c0d52d5d6de037
|
7
|
+
data.tar.gz: cda93559f8d2c47aaa4447d51d6f2151a006e3249e1297f379d041402c0f168f209acfda6003e42e30859ad901063e1aa0c5973127306ef7fdfec29ef7c058de
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Description #
|
2
|
+
|
3
|
+
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. Iggy generates InSpec AWS controls by mapping Terraform resources to InSpec resources. You may also use tags to annotate your Terraform scripts to specify which compliance profiles to be used and Iggy will create a profile including those dependencies.
|
4
|
+
|
5
|
+
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 [Terraform with InSpec](http://lollyrock.com/articles/inspec-terraform/).
|
6
|
+
|
7
|
+
The [CHANGELOG.md](https://github.com/mattray/iggy/blob/master/CHANGELOG.md) covers current, previous and future development milestones and contains the features backlog.
|
8
|
+
|
9
|
+
# Requirements #
|
10
|
+
|
11
|
+
Iggy generates compliance profiles for InSpec 2, which includes the AWS and Azure resources. Because resources are continuing to be added to InSpec, you may want the latest version to support as many resource coverage as possible.
|
12
|
+
|
13
|
+
Written and tested with Ruby 2.4.4 (or whatever InSpec 2.0 supports).
|
14
|
+
|
15
|
+
# Installation #
|
16
|
+
|
17
|
+
`inspec-iggy` is a plugin for InSpec and may be installed as follows
|
18
|
+
|
19
|
+
```bash
|
20
|
+
# install InSpec
|
21
|
+
gem install inspec
|
22
|
+
gem install inspec-iggy
|
23
|
+
inspec terraform version
|
24
|
+
```
|
25
|
+
|
26
|
+
## * for development: ##
|
27
|
+
|
28
|
+
```bash
|
29
|
+
# Install `inspec-iggy` via a symlink:
|
30
|
+
git clone git@github.com:inspec/inspec-iggy ~/inspec-iggy
|
31
|
+
mkdir -p ~/.inspec/plugins
|
32
|
+
ln -s ~/inspec-iggy/ ~/.inspec/plugins/inspec-iggy
|
33
|
+
inspec terraform version
|
34
|
+
```
|
35
|
+
|
36
|
+
## * or build a gem: ##
|
37
|
+
|
38
|
+
```bash
|
39
|
+
# Build the `inspec-iggy` then install:
|
40
|
+
git clone https://github.com/inspec/inspec-iggy && cd inspec-iggy && gem build *gemspec && gem install *gem
|
41
|
+
inspec terraform version
|
42
|
+
```
|
43
|
+
|
44
|
+
# InSpec Terraform Generate #
|
45
|
+
|
46
|
+
inspec terraform generate --tfstate terraform.tfstate
|
47
|
+
|
48
|
+
Iggy dynamically pulls the available AWS resources from InSpec and attempts to map them to the Terraform resources. Newer versions of InSpec may provide additional coverage.
|
49
|
+
|
50
|
+
# InSpec Terraform Extract (EXPERIMENTAL)#
|
51
|
+
|
52
|
+
inspec terraform extract --tfstate terraform.tfstate
|
53
|
+
|
54
|
+
## Tagging Profiles for Extract ##
|
55
|
+
|
56
|
+
Compliance profiles are added to the Terraform Resource to be tested. The current 2 options are the ```aws_vpc``` or the ```aws_instance```. By tagging the ```aws_vpc``` you are specifying that the test is against the AWS API rather than individual machines. AWS instances tagged with compliance profiles will attempt to form command lines for ```inspec exec``` against them.
|
57
|
+
|
58
|
+
### Tagging Format ###
|
59
|
+
|
60
|
+
Given there is not support for lists within AWS tags, we use the convention of starting our tag names with ```inspec_name_``` and ```inspec_url_```. These are extracted and split to identify the relevant compliance profiles to run.
|
61
|
+
|
62
|
+
```
|
63
|
+
tags {
|
64
|
+
iggy_name_apache_baseline = "apache-baseline",
|
65
|
+
iggy_url_apache_baseline = "https://github.com/dev-sec/apache-baseline",
|
66
|
+
iggy_name_linux_baseline = "linux-baseline",
|
67
|
+
iggy_url_linux_baseline = "https://github.com/dev-sec/linux-baseline"
|
68
|
+
}
|
69
|
+
```
|
70
|
+
|
71
|
+
### Potential Enhancements ###
|
72
|
+
|
73
|
+
The current tagging for extraction implementation is directly tied to AWS. Other platforms such as Azure undoubtedly behave differently. Longterm this functionality should probably be turned into a Terraform Provider with predefined outputs.
|
74
|
+
|
75
|
+
Subnet might be a better choice for tagging than VPCs, given they list the AZ.
|
76
|
+
|
77
|
+
Currently it only supports URL-based compliance profiles. InSpec supports other formats (git, path, supermarket, compliance).
|
78
|
+
|
79
|
+
inspec exec https://github.com/dev-sec/linux-baseline -t ssh://clckwrk@52.33.203.34 -i ~/.ssh/mattray-apac
|
80
|
+
|
81
|
+
# CloudFormation Support #
|
82
|
+
|
83
|
+
**CloudFormation support has been started, but it is incomplete while focusing on Terraform.** Here is an example of the current output, note that it's not tied to an actual deployed CloudFormation Stack, so that will need to be provided for the entry point of testing.
|
84
|
+
https://gist.github.com/c4d6eda82dfb25502ef381cc631a1edd
|
85
|
+
|
86
|
+
# Testing #
|
87
|
+
|
88
|
+
Iggy uses [RSpec](http://rspec.info/) for testing. You should run the following before committing.
|
89
|
+
|
90
|
+
$ rspec
|
91
|
+
|
92
|
+
For style
|
93
|
+
|
94
|
+
$ chefstyle .
|
95
|
+
|
96
|
+
# License and Author #
|
97
|
+
|
98
|
+
| | |
|
99
|
+
|:---------------|:------------------------------------------|
|
100
|
+
| **Author** | Matt Ray (<matt@chef.io>) |
|
101
|
+
| **Copyright:** | Copyright (c) 2017 Chef Software Inc. |
|
102
|
+
| **License:** | Apache License, Version 2.0 |
|
103
|
+
|
104
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
105
|
+
you may not use this file except in compliance with the License.
|
106
|
+
You may obtain a copy of the License at
|
107
|
+
|
108
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
109
|
+
|
110
|
+
Unless required by applicable law or agreed to in writing, software
|
111
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
112
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
113
|
+
See the License for the specific language governing permissions and
|
114
|
+
limitations under the License.
|
data/inspec-iggy.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
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 = 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."
|
13
|
+
spec.description = "Generate InSpec compliance profiles from Terraform by tagging instances and mapping Terraform to InSpec."
|
14
|
+
spec.homepage = "https://github.com/inspec/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,test}/**/*", File::FNM_DOTMATCH
|
21
|
+
).reject { |f| File.directory?(f) }
|
22
|
+
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency "inspec", ">=2.0", "<3.0.0"
|
26
|
+
end
|
data/lib/inspec-iggy.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Matt Ray (<matt@chef.io>)
|
4
|
+
#
|
5
|
+
# Copyright:: 2018, Chef Software, Inc <legal@chef.io>
|
6
|
+
#
|
7
|
+
|
8
|
+
require "inspec/plugins"
|
9
|
+
require "thor"
|
10
|
+
|
11
|
+
require "inspec-iggy/terraform"
|
12
|
+
|
13
|
+
module Iggy
|
14
|
+
class CLI < Thor
|
15
|
+
namespace "terraform"
|
16
|
+
|
17
|
+
map %w{-v --version} => "version"
|
18
|
+
|
19
|
+
desc "version", "Display version information", hide: true
|
20
|
+
def version
|
21
|
+
say("Iggy v#{Iggy::VERSION}")
|
22
|
+
end
|
23
|
+
|
24
|
+
class_option :tfstate,
|
25
|
+
:aliases => "-t",
|
26
|
+
:desc => "Specify path to the input terraform.tfstate",
|
27
|
+
:default => "terraform.tfstate"
|
28
|
+
|
29
|
+
class_option :debug,
|
30
|
+
:desc => "Verbose debugging messages",
|
31
|
+
:type => :boolean,
|
32
|
+
:default => false
|
33
|
+
|
34
|
+
desc "generate [options]", "Generate InSpec compliance controls from terraform.tfstate"
|
35
|
+
def generate
|
36
|
+
Inspec::Log.level = :debug if options[:debug]
|
37
|
+
generated_controls = Iggy::Terraform.parse_generate(options[:tfstate])
|
38
|
+
# let's just generate a control file with a set of controls for now
|
39
|
+
Iggy::InspecHelper.print_controls(options[:tfstate], generated_controls)
|
40
|
+
exit 0
|
41
|
+
end
|
42
|
+
|
43
|
+
desc "extract [options]", "Extract tagged InSpec profiles from terraform.tfstate"
|
44
|
+
def extract
|
45
|
+
Inspec::Log.level = :debug if options[:debug]
|
46
|
+
extracted_profiles = Iggy::Terraform.parse_extract(options[:tfstate])
|
47
|
+
Iggy::InspecHelper.print_commands(extracted_profiles)
|
48
|
+
exit 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Inspec::Plugins::CLI.add_subcommand(CLI, "terraform", "terraform SUBCOMMAND ...", "Extract or generate InSpec from Terraform", {})
|
53
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Matt Ray (<matt@chef.io>)
|
3
|
+
#
|
4
|
+
# Copyright:: 2018, Chef Software, Inc <legal@chef.io>
|
5
|
+
#
|
6
|
+
|
7
|
+
require "iggy"
|
8
|
+
|
9
|
+
require "json"
|
10
|
+
require "thor"
|
11
|
+
|
12
|
+
module Iggy
|
13
|
+
class CloudFormation < Thor
|
14
|
+
option :template,
|
15
|
+
:aliases => "-t",
|
16
|
+
:desc => "Specify path to the input CloudFormation template"
|
17
|
+
|
18
|
+
option :debug,
|
19
|
+
:desc => "Verbose debugging messages",
|
20
|
+
:type => :boolean,
|
21
|
+
:default => false
|
22
|
+
|
23
|
+
desc "generate [options]", "Generate InSpec compliance controls from CloudFormation template"
|
24
|
+
long_desc <<-LONGDESC
|
25
|
+
Reads in a CloudFormation JSON file and generates matching InSpec compliance controls.
|
26
|
+
LONGDESC
|
27
|
+
def generate
|
28
|
+
Iggy::Log.level = :debug if options[:debug]
|
29
|
+
Iggy::Log.debug "CloudFormation.generate file = #{options[:template]}"
|
30
|
+
# hash of generated controls
|
31
|
+
generated_controls = parse_generate(options[:template])
|
32
|
+
Iggy::Log.debug "CloudFormation.generate generated_controls = #{generated_controls}"
|
33
|
+
# let's just generate a control file with a set of controls for now
|
34
|
+
Iggy::Inspec.print_controls(options[:template], generated_controls)
|
35
|
+
exit 0
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def parse_generate(file)
|
41
|
+
Iggy::Log.debug "CloudFormation.parse_generate file = #{file}"
|
42
|
+
begin
|
43
|
+
unless File.file?(file)
|
44
|
+
STDERR.puts "ERROR: #{file} is an invalid file, please check your path."
|
45
|
+
exit(-1)
|
46
|
+
end
|
47
|
+
template = JSON.parse(File.read(file))
|
48
|
+
rescue JSON::ParserError => e
|
49
|
+
STDERR.puts e.message
|
50
|
+
STDERR.puts "ERROR: Parsing error in #{file}."
|
51
|
+
exit(-1)
|
52
|
+
end
|
53
|
+
basename = File.basename(file)
|
54
|
+
absolutename = File.absolute_path(file)
|
55
|
+
|
56
|
+
# InSpec controls generated
|
57
|
+
generated_controls = {}
|
58
|
+
|
59
|
+
# iterate over the resources
|
60
|
+
cfn_resources = template["Resources"]
|
61
|
+
# iterate over the Resources, use these as IDs?
|
62
|
+
cfn_resources.keys.each do |cfn_res|
|
63
|
+
# split out the last ::, these are all AWS
|
64
|
+
cfn_res_type = "aws_" + cfn_resources[cfn_res]["Type"].split("::").last.downcase
|
65
|
+
|
66
|
+
# does this match an InSpec resource?
|
67
|
+
if Inspec::RESOURCES.include?(cfn_res_type)
|
68
|
+
Iggy::Log.debug "CloudFormation.parse_generate cfn_res_type = #{cfn_res_type} MATCH"
|
69
|
+
# insert new control based off the resource's ID
|
70
|
+
generated_controls[cfn_res] = {}
|
71
|
+
generated_controls[cfn_res]["name"] = "#{cfn_res_type}::#{cfn_res}"
|
72
|
+
generated_controls[cfn_res]["title"] = "Iggy #{basename} #{cfn_res_type}::#{cfn_res}"
|
73
|
+
generated_controls[cfn_res]["desc"] = "#{cfn_res_type}::#{cfn_res} from the source file #{absolutename}\nGenerated by Iggy v#{Iggy::VERSION}"
|
74
|
+
generated_controls[cfn_res]["impact"] = "1.0"
|
75
|
+
generated_controls[cfn_res]["resource"] = cfn_res_type
|
76
|
+
generated_controls[cfn_res]["parameter"] = cfn_res
|
77
|
+
generated_controls[cfn_res]["tests"] = []
|
78
|
+
generated_controls[cfn_res]["tests"][0] = "it { should exist }"
|
79
|
+
|
80
|
+
# if there's a match, see if there are matching InSpec properties
|
81
|
+
inspec_properties = Iggy::Inspec.resource_properties(cfn_res_type)
|
82
|
+
|
83
|
+
cfn_resources[cfn_res]["Properties"].keys.each do |attr|
|
84
|
+
# insert '_' on the CamelCase to get camel_case
|
85
|
+
attr_split = attr.split /(?=[A-Z])/
|
86
|
+
property = attr_split.join("_").downcase
|
87
|
+
if inspec_properties.member?(property)
|
88
|
+
Iggy::Log.debug "CloudFormation.parse_generate #{cfn_res_type} inspec_property = #{property} MATCH"
|
89
|
+
value = cfn_resources[cfn_res]["Properties"][attr]
|
90
|
+
# skip the {"Ref"=>"VPC"} for now
|
91
|
+
generated_controls[cfn_res]["tests"].push("its('#{property}') { should cmp '#{value}' }") unless value.is_a? Hash
|
92
|
+
else
|
93
|
+
Iggy::Log.debug "CloudFormation.parse_generate #{cfn_res_type} inspec_property = #{property} SKIP"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
else
|
97
|
+
Iggy::Log.debug "CloudFormation.parse_generate cfn_res_type = #{cfn_res_type} SKIP"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
generated_controls
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Matt Ray (<matt@chef.io>)
|
3
|
+
#
|
4
|
+
# Copyright:: 2018, Chef Software, Inc <legal@chef.io>
|
5
|
+
#
|
6
|
+
|
7
|
+
require "inspec"
|
8
|
+
|
9
|
+
module Iggy
|
10
|
+
class InspecHelper
|
11
|
+
|
12
|
+
# constants for the InSpec resources
|
13
|
+
RESOURCES = Inspec::Resource.registry.keys
|
14
|
+
|
15
|
+
# translate Terraform resource name to InSpec
|
16
|
+
TERRAFORM_RESOURCES = {
|
17
|
+
"aws_instance" => "aws_ec2_instance",
|
18
|
+
# 'aws_route' => 'aws_route_table' # needs route_table_id instead of id
|
19
|
+
}
|
20
|
+
|
21
|
+
# # there really should be some way to get this directly from InSpec's resources
|
22
|
+
def self.resource_properties(resource)
|
23
|
+
# remove the common methods, in theory only leaving only unique InSpec properties
|
24
|
+
inspec_properties = Inspec::Resource.registry[resource].instance_methods - COMMON_PROPERTIES
|
25
|
+
# get InSpec properties by method names
|
26
|
+
inspec_properties.collect! { |x| x.to_s }
|
27
|
+
Inspec::Log.debug "Iggy::InspecHelper.resource_properties #{resource} properties = #{inspec_properties}"
|
28
|
+
|
29
|
+
inspec_properties
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.print_commands(extracted_profiles)
|
33
|
+
extracted_profiles.keys.each do |cmd|
|
34
|
+
type = extracted_profiles[cmd]["type"]
|
35
|
+
url = extracted_profiles[cmd]["url"]
|
36
|
+
key_name = extracted_profiles[cmd]["key_name"]
|
37
|
+
if type == "aws_instance"
|
38
|
+
ip = extracted_profiles[cmd]["public_ip"]
|
39
|
+
puts "inspec exec #{url} -t ssh://#{ip} -i #{key_name}"
|
40
|
+
else
|
41
|
+
puts "inspec exec #{url} -t aws://us-west-2"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.print_controls(file, generated_controls)
|
47
|
+
puts "# encoding: utf-8\n#"
|
48
|
+
|
49
|
+
puts "\ntitle '#{File.absolute_path(file)} controls generated by Iggy v#{Iggy::VERSION}'"
|
50
|
+
|
51
|
+
# write all controls
|
52
|
+
puts generated_controls.flatten.map(&:to_ruby).join("\n\n")
|
53
|
+
end
|
54
|
+
|
55
|
+
# a hack for sure, finds common methods as proxy for InSpec properties
|
56
|
+
COMMON_PROPERTIES = Inspec::Resource.registry["aws_subnet"].instance_methods &
|
57
|
+
Inspec::Resource.registry["directory"].instance_methods
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Matt Ray (<matt@chef.io>)
|
3
|
+
#
|
4
|
+
# Copyright:: 2018, Chef Software, Inc <legal@chef.io>
|
5
|
+
#
|
6
|
+
|
7
|
+
require "inspec/objects/control"
|
8
|
+
require "inspec/objects/ruby_helper"
|
9
|
+
require "inspec/objects/describe"
|
10
|
+
|
11
|
+
require "inspec-iggy/inspec_helper"
|
12
|
+
|
13
|
+
module Iggy
|
14
|
+
class Terraform
|
15
|
+
|
16
|
+
# makes it easier to change out later
|
17
|
+
TAG_NAME = "iggy_name_"
|
18
|
+
TAG_URL = "iggy_url_"
|
19
|
+
|
20
|
+
# boilerplate tfstate parsing
|
21
|
+
def self.parse_tfstate(file)
|
22
|
+
Inspec::Log.debug "Iggy::Terraform.parse_tfstate file = #{file}"
|
23
|
+
begin
|
24
|
+
unless File.file?(file)
|
25
|
+
STDERR.puts "ERROR: #{file} is an invalid file, please check your path."
|
26
|
+
exit(-1)
|
27
|
+
end
|
28
|
+
tfstate = JSON.parse(File.read(file))
|
29
|
+
rescue JSON::ParserError => e
|
30
|
+
STDERR.puts e.message
|
31
|
+
STDERR.puts "ERROR: Parsing error in #{file}."
|
32
|
+
exit(-1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# parse through the JSON for the tagged Resources
|
37
|
+
def self.parse_extract(file)
|
38
|
+
tfstate = parse_tfstate(file)
|
39
|
+
# InSpec profiles extracted
|
40
|
+
extracted_profiles = {}
|
41
|
+
|
42
|
+
# iterate over the resources
|
43
|
+
tf_resources = tfstate["modules"][0]["resources"]
|
44
|
+
tf_resources.keys.each do |tf_res|
|
45
|
+
tf_res_id = tf_resources[tf_res]["primary"]["id"]
|
46
|
+
|
47
|
+
# get the attributes, see if any of them have a tagged profile attached
|
48
|
+
tf_resources[tf_res]["primary"]["attributes"].keys.each do |attr|
|
49
|
+
next unless attr.start_with?("tags." + TAG_NAME)
|
50
|
+
Inspec::Log.debug "Iggy::Terraform.parse_extract tf_res = #{tf_res} attr = #{attr} MATCHED TAG"
|
51
|
+
# get the URL and the name of the profiles
|
52
|
+
name = attr.split(TAG_NAME)[1]
|
53
|
+
url = tf_resources[tf_res]["primary"]["attributes"]["tags.#{TAG_URL}#{name}"]
|
54
|
+
if tf_res.start_with?("aws_vpc") # should this be VPC or subnet?
|
55
|
+
# if it's a VPC, store it as the VPC id + name
|
56
|
+
key = tf_res_id + ":" + name
|
57
|
+
Inspec::Log.debug "Iggy::Terraform.parse_extract aws_vpc tagged with InSpec #{key}"
|
58
|
+
extracted_profiles[key] = {
|
59
|
+
"type" => "aws_vpc",
|
60
|
+
"az" => "us-west-2",
|
61
|
+
"url" => url,
|
62
|
+
}
|
63
|
+
elsif tf_res.start_with?("aws_instance")
|
64
|
+
# if it's a node, get information about the IP and SSH/WinRM
|
65
|
+
key = tf_res_id + ":" + name
|
66
|
+
Inspec::Log.debug "Iggy::Terraform.parse_extract aws_instance tagged with InSpec #{key}"
|
67
|
+
extracted_profiles[key] = {
|
68
|
+
"type" => "aws_instance",
|
69
|
+
"public_ip" => tf_resources[tf_res]["primary"]["attributes"]["public_ip"],
|
70
|
+
"key_name" => tf_resources[tf_res]["primary"]["attributes"]["key_name"],
|
71
|
+
"url" => url,
|
72
|
+
}
|
73
|
+
else
|
74
|
+
# should generic AWS just be the default except for instances?
|
75
|
+
STDERR.puts "ERROR: #{file} #{tf_res_id} has an InSpec-tagged resource but #{tf_res} is currently unsupported."
|
76
|
+
exit(-1)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
Inspec::Log.debug "Iggy::Terraform.parse_extract extracted_profiles = #{extracted_profiles}"
|
81
|
+
extracted_profiles
|
82
|
+
end
|
83
|
+
|
84
|
+
# parse through the JSON and generate InSpec controls
|
85
|
+
def self.parse_generate(file)
|
86
|
+
tfstate = parse_tfstate(file)
|
87
|
+
basename = File.basename(file)
|
88
|
+
absolutename = File.absolute_path(file)
|
89
|
+
|
90
|
+
# InSpec controls generated
|
91
|
+
generated_controls = []
|
92
|
+
|
93
|
+
# iterate over the resources
|
94
|
+
tf_resources = tfstate["modules"][0]["resources"]
|
95
|
+
tf_resources.keys.each do |tf_res|
|
96
|
+
tf_res_type = tf_resources[tf_res]["type"]
|
97
|
+
|
98
|
+
# add translation layer
|
99
|
+
if InspecHelper::TERRAFORM_RESOURCES.keys.include?(tf_res_type)
|
100
|
+
Inspec::Log.debug "Iggy::Terraform.parse_generate tf_res_type = #{tf_res_type} #{InspecHelper::TERRAFORM_RESOURCES[tf_res_type]} TRANSLATED"
|
101
|
+
tf_res_type = InspecHelper::TERRAFORM_RESOURCES[tf_res_type]
|
102
|
+
end
|
103
|
+
|
104
|
+
# does this match an InSpec resource?
|
105
|
+
if InspecHelper::RESOURCES.include?(tf_res_type)
|
106
|
+
Inspec::Log.debug "Iggy::Terraform.parse_generate tf_res_type = #{tf_res_type} MATCH"
|
107
|
+
tf_res_id = tf_resources[tf_res]["primary"]["id"]
|
108
|
+
|
109
|
+
# insert new control based off the resource's ID
|
110
|
+
ctrl = Inspec::Control.new
|
111
|
+
ctrl.id = "#{tf_res_type}::#{tf_res_id}"
|
112
|
+
ctrl.title = "Iggy #{basename} #{tf_res_type}::#{tf_res_id}"
|
113
|
+
ctrl.desc = "#{tf_res_type}::#{tf_res_id} from the source file #{absolutename}\nGenerated by Iggy v#{Iggy::VERSION}"
|
114
|
+
ctrl.impact = "1.0"
|
115
|
+
|
116
|
+
describe = Inspec::Describe.new
|
117
|
+
# describes the resourde with the id as argument
|
118
|
+
describe.qualifier.push([tf_res_type, tf_res_id])
|
119
|
+
|
120
|
+
# ensure the resource exists
|
121
|
+
describe.add_test(nil, "exist", nil)
|
122
|
+
|
123
|
+
# if there's a match, see if there are matching InSpec properties
|
124
|
+
inspec_properties = Iggy::InspecHelper.resource_properties(tf_res_type)
|
125
|
+
tf_resources[tf_res]["primary"]["attributes"].keys.each do |attr|
|
126
|
+
if inspec_properties.member?(attr)
|
127
|
+
Inspec::Log.debug "Iggy::Terraform.parse_generate #{tf_res_type} inspec_property = #{attr} MATCH"
|
128
|
+
value = tf_resources[tf_res]["primary"]["attributes"][attr]
|
129
|
+
describe.add_test(attr, "cmp", value)
|
130
|
+
else
|
131
|
+
Inspec::Log.debug "Iggy::Terraform.parse_generate #{tf_res_type} inspec_property = #{attr} SKIP"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
ctrl.add_test(describe)
|
136
|
+
generated_controls.push(ctrl)
|
137
|
+
else
|
138
|
+
Inspec::Log.debug "Iggy::Terraform.parse_generate tf_res_type = #{tf_res_type} SKIP"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
Inspec::Log.debug "Iggy::Terraform.parse_generate generated_controls = #{generated_controls}"
|
142
|
+
generated_controls
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|