inspec-chef 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d92257a6fe51e2e753b38cec027da0e1d81578cf9cd96bc941e17ffbf2957ab
4
- data.tar.gz: 93d8dea2ff6a1f8d8040543e4bccb4316001c017459cb392f12edaf49ec33633
3
+ metadata.gz: 29c070e9e924069b6c2f7418bdfd8164b0c69f9ae3ad7447928b3cdb99ca6e30
4
+ data.tar.gz: 875794f04d148b86a41a401c481bc0fb53e6b398059668fc1d307820e4b285ff
5
5
  SHA512:
6
- metadata.gz: c43fc11316341123dfff07d7eb1d8afe4515d53ab4f0bb7c8d36cb5e7f94683dad316be341d79fbd3f484f23dabdd554e5ae17bacef97e6b02fa17fc8422c09c
7
- data.tar.gz: de6d24d7ed8d75e6f297d9ca30605db58009308b1e09397f9f8c46413f173037501cec8afb2533665fb7b98b7e2d881e056d4ce77b748d824b36f7275142fab7
6
+ metadata.gz: e87dee841335d6e05a96cdb2d74bd8a77b7b975d9a54f73a0eae94766cb7def48adc20291ea821d0b3d51096b9cb4b5ad1419d8105f9f253a784d22f06b42f02
7
+ data.tar.gz: 47360a9eb493ddf659535f7f59b3810bd271a2ba49b3e84677e3cc4c6e936b7273eb764d211fc7d8d4ad6272cc2a027311f46c4690eb7aabd4cff03637a8fcd2
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ group :development do
9
9
  gem "chefstyle", "0.13.0"
10
10
  gem "m"
11
11
  gem "bundler"
12
+ gem "minitest"
12
13
  gem "rake"
13
14
  gem "rubocop"
14
15
  end
@@ -38,4 +38,5 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "jmespath", "~> 1.4"
39
39
 
40
40
  spec.add_development_dependency "bump", "~> 0.8"
41
+ spec.add_development_dependency "minitest", "~> 5.11"
41
42
  end
@@ -3,191 +3,223 @@ require "jmespath"
3
3
  require "resolv"
4
4
  require "uri"
5
5
 
6
- module InspecPlugins::Chef
7
- class Input < Inspec.plugin(2, :input)
8
- VALID_PATTERNS = [
9
- Regexp.new("^databag://[^/]+/[^/]+/.+$"),
10
- Regexp.new("^node://[^/]*/attributes/.+$"),
11
- ].freeze
12
-
13
- attr_reader :plugin_conf, :chef_endpoint, :chef_client, :chef_api_key
14
- attr_reader :chef_api
15
-
16
- # Set up new class
17
- def initialize
18
- @plugin_conf = Inspec::Config.cached.fetch_plugin_config("inspec-chef")
19
- end
6
+ module InspecPlugins
7
+ module Chef
8
+ class Input < Inspec.plugin(2, :input)
9
+ VALID_PATTERNS = [
10
+ Regexp.new("^databag://[^/]+/[^/]+/.+$"),
11
+ Regexp.new("^node://[^/]*/attributes/.+$"),
12
+ ].freeze
13
+
14
+ attr_reader :chef_server
20
15
 
21
- # Fetch method used for Input plugins
22
- def fetch(_profile_name, input_uri)
23
- return nil unless valid_plugin_input? input_uri
16
+ # ========================================================================
17
+ # Dependency Injection
24
18
 
25
- connect_to_chef_server
19
+ attr_writer :inspec_config, :logger
26
20
 
27
- input = parse_input(input_uri)
28
- if input[:type] == :databag
29
- data = get_databag_item(input[:object], input[:item])
30
- elsif input[:type] == :node
31
- data = get_attributes(input[:object]) if input[:item] == "attributes"
21
+ def inspec_config
22
+ @inspec_config ||= Inspec::Config.cached
32
23
  end
33
24
 
34
- result = JMESPath.search(input[:query].join("."), data)
35
- raise format("Could not resolve value for %s, check if databag/item or attribute exist", input_uri) if result.nil?
25
+ def logger
26
+ @logger ||= Inspec::Log
27
+ end
36
28
 
37
- result
38
- end
29
+ # ========================================================================
30
+ # Input Plugin API
39
31
 
40
- private
32
+ # Fetch method used for Input plugins
33
+ def fetch(_profile_name, input_uri)
34
+ return nil unless valid_plugin_input?(input_uri)
41
35
 
42
- # Check if this is called from within TestKitchen
43
- def inside_testkitchen?
44
- !! defined?(::Kitchen::Logger)
45
- end
36
+ connect_to_chef_server
46
37
 
47
- # Check if this is an IP
48
- def ip?(ip_or_name)
49
- # Get address always returns an IP, so if nothing changes this was one
50
- Resolv.getaddress(ip_or_name) == ip_or_name
51
- rescue Resolv::ResolvError
52
- false
53
- end
38
+ input = parse_input(input_uri)
39
+ if input[:type] == :databag
40
+ data = get_databag_item(input[:object], input[:item])
41
+ elsif input[:type] == :node && input[:item] == "attributes"
42
+ # Search Chef node name, if no host given explicitly
43
+ input[:object] = get_clientname(scan_target) unless input[:object]
54
44
 
55
- # Check if this is an FQDN
56
- def fqdn?(ip_or_name)
57
- # If it is not an IP but contains a Dot, it is an FQDN
58
- !ip?(ip_or_name) && ip_or_name.include?(".")
59
- end
45
+ data = get_attributes(input[:object])
46
+ end
60
47
 
61
- # Reach for Kitchen data and return its evaluated config
62
- def kitchen_provisioner_config
63
- require "binding_of_caller"
64
- kitchen = binding.callers.find { |b| b.frame_description == "verify" }.receiver
48
+ result = JMESPath.search(input[:query].join("."), data)
49
+ raise format("Could not resolve value for %s, check if databag/item or attribute exist", input_uri) unless result
65
50
 
66
- kitchen.provisioner.send(:provided_config)
67
- end
51
+ result
52
+ end
68
53
 
69
- # Get plugin setting via environment, config file or default
70
- def fetch_plugin_setting(setting_name, default = nil)
71
- env_var_name = "INSPEC_CHEF_#{setting_name.upcase}"
72
- config_name = "chef_api_#{setting_name.downcase}"
73
- ENV[env_var_name] || plugin_conf[config_name] || default
74
- end
54
+ private
75
55
 
76
- # Get remote address for this scan from InSpec
77
- def inspec_target
78
- target = Inspec::Config.cached.final_options["target"]
79
- URI.parse(target)&.host
80
- end
56
+ # ========================================================================
57
+ # Helper Methods
81
58
 
82
- # Establish a Chef Server connection
83
- def connect_to_chef_server
84
- # From within TestKitchen we need no Chef Server connection
85
- if defined?(Kitchen)
86
- Inspec::Log.info "Running from TestKitchen, using provisioner settings instead of Chef Server"
59
+ # Check if this is called from within TestKitchen
60
+ def inside_testkitchen?
61
+ !! defined?(::Kitchen)
62
+ end
87
63
 
88
- # Only connect once
89
- elsif !connected?
90
- @chef_endpoint = fetch_plugin_setting("endpoint")
91
- @chef_client = fetch_plugin_setting("client")
92
- @chef_api_key = fetch_plugin_setting("key")
64
+ # Check if this is an IP
65
+ def ip?(ip_or_name)
66
+ # Get address always returns an IP, so if nothing changes this was one
67
+ Resolv.getaddress(ip_or_name) == ip_or_name
68
+ rescue Resolv::ResolvError
69
+ false
70
+ end
93
71
 
94
- if chef_endpoint.nil? || chef_client.nil? || chef_api_key.nil?
95
- raise "ERROR: Plugin inspec-chef needs configuration of chef endpoint, client name and api key."
96
- end
72
+ # Check if this is an FQDN
73
+ def fqdn?(ip_or_name)
74
+ # If it is not an IP but contains a Dot, it is an FQDN
75
+ !ip?(ip_or_name) && ip_or_name.include?(".")
76
+ end
97
77
 
98
- @chef_api ||= ChefAPI::Connection.new(
99
- endpoint: chef_endpoint,
100
- client: chef_client,
101
- key: chef_api_key
102
- )
78
+ # Merge attributes in hierarchy like Chef
79
+ def merge_attributes(data)
80
+ data.fetch("default", {})
81
+ .merge(data.fetch("normal", {}))
82
+ .merge(data.fetch("override", {}))
83
+ .merge(data.fetch("automatic", {}))
84
+ end
103
85
 
104
- Inspec::Log.debug format("Connected to %s as client %s", chef_endpoint, chef_client)
86
+ # Verify if input is valid for this plugin
87
+ def valid_plugin_input?(input)
88
+ VALID_PATTERNS.any? { |regex| regex.match? input }
105
89
  end
106
- end
107
90
 
108
- # Return if connection is established
109
- def connected?
110
- ! @chef_api.nil?
111
- end
91
+ # Parse InSpec input name into Databag, Item and search query
92
+ def parse_input(input_uri)
93
+ uri = URI(input_uri)
94
+ item, *components = uri.path.slice(1..-1).split("/")
95
+
96
+ {
97
+ type: uri.scheme.to_sym,
98
+ object: uri.host,
99
+ item: item,
100
+ query: components,
101
+ }
102
+ end
112
103
 
113
- # Retrieve a Databag item from Chef Server
114
- def get_databag_item(databag, item)
115
- unless inside_testkitchen?
116
- unless chef_api.data_bags.any? { |k| k.name == databag }
117
- raise format('Databag "%s" not found on Chef Infra Server', databag)
118
- end
104
+ # ========================================================================
105
+ # Interfacing with Inspec and Chef
106
+
107
+ # Reach for Kitchen data and return its evaluated config
108
+ # @todo DI
109
+ def kitchen_provisioner_config
110
+ require "binding_of_caller"
111
+ kitchen = binding.callers.find { |b| b.frame_description == "verify" }.receiver
112
+
113
+ kitchen.provisioner.send(:provided_config)
114
+ end
119
115
 
120
- chef_api.data_bag_item.fetch(item, bag: databag).data
121
- else
122
- config = kitchen_provisioner_config
123
- filename = File.join(config[:data_bags_path], databag, item + ".json")
116
+ # Get plugin specific configuration
117
+ def plugin_conf
118
+ inspec_config.fetch_plugin_config("inspec-chef")
119
+ end
120
+
121
+ # Get plugin setting via environment, config file or default
122
+ def fetch_plugin_setting(setting_name, default = nil)
123
+ env_var_name = "INSPEC_CHEF_#{setting_name.upcase}"
124
+ config_name = "chef_api_#{setting_name.downcase}"
125
+ ENV[env_var_name] || plugin_conf[config_name] || default
126
+ end
127
+
128
+ # Get remote address for this scan from InSpec
129
+ def scan_target
130
+ target = inspec_config.final_options["target"]
131
+ URI.parse(target)&.host
132
+ end
133
+
134
+ # Establish a Chef Server connection
135
+ def connect_to_chef_server
136
+ # From within TestKitchen we need no Chef Server connection
137
+ if inside_testkitchen?
138
+ logger.info "Running from TestKitchen, using provisioner settings instead of Chef Server"
124
139
 
125
- begin
126
- return JSON.load(File.read(filename))
127
- rescue
128
- raise format("Error accessing databag file %s, check TestKitchen configuration", filename)
140
+ # Only connect once
141
+ elsif !server_connected?
142
+ @plugin_conf = inspec_config.fetch_plugin_config("inspec-chef")
143
+
144
+ chef_endpoint = fetch_plugin_setting("endpoint")
145
+ chef_client = fetch_plugin_setting("client")
146
+ chef_api_key = fetch_plugin_setting("key")
147
+
148
+ unless chef_endpoint && chef_client && chef_api_key
149
+ raise "ERROR: Plugin inspec-chef needs configuration of chef endpoint, client name and api key."
150
+ end
151
+
152
+ # @todo: DI this
153
+ @chef_server ||= ChefAPI::Connection.new(
154
+ endpoint: chef_endpoint,
155
+ client: chef_client,
156
+ key: chef_api_key
157
+ )
158
+
159
+ logger.debug format("Connected to %s as client %s", chef_endpoint, chef_client)
129
160
  end
130
161
  end
131
- end
132
162
 
133
- # Retrieve attributes of a node
134
- def get_attributes(node)
135
- unless inside_testkitchen?
136
- data = get_search(:node, "name:#{node}")
163
+ # Return if connection is established
164
+ def server_connected?
165
+ ! chef_server.nil?
166
+ end
137
167
 
138
- merge_attributes(data)
139
- else
140
- kitchen_provisioner_config[:attributes]
168
+ # Low-level Chef search expression
169
+ def get_search(index, expression)
170
+ chef_server.search.query(index, expression, rows: 1).rows.first
141
171
  end
142
- end
143
172
 
144
- # Try to look up Chef Client name by the address requested
145
- def get_clientname(ip_or_name)
146
- query = "hostname:%<address>s"
147
- query = "ipaddress:%<address>s" if ip?(ip_or_name)
148
- query = "fqdn:%<address>s" if fqdn?(ip_or_name)
149
- result = get_search(:node, format(query, address: ip_or_name))
150
- Inspec::Log.debug format("Automatic lookup of node name (IPv4 or hostname) returned: %s", result&.fetch("name") || "(nothing)")
151
-
152
- # Try EC2 lookup, if nothing found (assuming public IP)
153
- if result.nil?
154
- query = "ec2_public_ipv4:%<address>s OR ec2_public_hostname:%<address>s"
155
- result = get_search(:node, format(query, address: ip_or_name))
156
- Inspec::Log.debug format("Automatic lookup of node name (EC2 public IPv4 or hostname) returned: %s", result&.fetch("name"))
173
+ # Retrieve a Databag item from Chef Server
174
+ def get_databag_item(databag, item)
175
+ unless inside_testkitchen?
176
+ unless chef_server.data_bags.any? { |k| k.name == databag }
177
+ raise format('Databag "%s" not found on Chef Infra Server', databag)
178
+ end
179
+
180
+ chef_server.data_bag_item.fetch(item, bag: databag).data
181
+ else
182
+ config = kitchen_provisioner_config
183
+ filename = File.join(config[:data_bags_path], databag, item + ".json")
184
+
185
+ begin
186
+ return JSON.load(File.read(filename))
187
+ rescue
188
+ raise format("Error accessing databag file %s, check TestKitchen configuration", filename)
189
+ end
190
+ end
157
191
  end
158
192
 
159
- # This will fail for cases like trying to connect to IPv6, so it will
160
- # need extension in the future
193
+ # Retrieve attributes of a node
194
+ def get_attributes(node)
195
+ unless inside_testkitchen?
196
+ data = get_search(:node, "name:#{node}")
161
197
 
162
- result&.fetch("name") || raise(format("Unable too lookup remote Chef client name from %s", ip_or_name))
163
- end
198
+ merge_attributes(data)
199
+ else
200
+ kitchen_provisioner_config[:attributes]
201
+ end
202
+ end
164
203
 
165
- # Low-level Chef search expression
166
- def get_search(index, expression)
167
- chef_api.search.query(index, expression, rows: 1).rows.first
168
- end
204
+ # Try to look up Chef Client name by the address requested
205
+ def get_clientname(ip_or_name)
206
+ query = "hostname:%<address>s"
207
+ query = "ipaddress:%<address>s" if ip?(ip_or_name)
208
+ query = "fqdn:%<address>s" if fqdn?(ip_or_name)
209
+ result = get_search(:node, format(query, address: ip_or_name))
210
+ logger.debug format("Automatic lookup of node name (IPv4 or hostname) returned: %s", result&.fetch("name") || "(nothing)")
169
211
 
170
- # Merge attributes in hierarchy like Chef
171
- def merge_attributes(data)
172
- data["default"].merge(data["normal"]).merge(data["override"]).merge(data["automatic"])
173
- end
212
+ # Try EC2 lookup, if nothing found (assuming public IP)
213
+ unless result
214
+ query = "ec2_public_ipv4:%<address>s OR ec2_public_hostname:%<address>s"
215
+ result = get_search(:node, format(query, address: ip_or_name))
216
+ logger.debug format("Automatic lookup of node name (EC2 public IPv4 or hostname) returned: %s", result&.fetch("name"))
217
+ end
174
218
 
175
- # Verify if input is valid for this plugin
176
- def valid_plugin_input?(input)
177
- VALID_PATTERNS.any? { |regex| regex.match? input }
178
- end
219
+ # This will fail for cases like trying to connect to IPv6, so it will need extension in the future
179
220
 
180
- # Parse InSpec input name into Databag, Item and search query
181
- def parse_input(input_uri)
182
- uri = URI(input_uri)
183
- item, *components = uri.path.slice(1..-1).split("/")
184
-
185
- {
186
- type: uri.scheme.to_sym,
187
- object: uri.host || get_clientname(inspec_target),
188
- item: item,
189
- query: components,
190
- }
221
+ result&.fetch("name") || raise(format("Unable too lookup remote Chef client name from %s", ip_or_name))
222
+ end
191
223
  end
192
224
  end
193
225
  end
@@ -5,6 +5,6 @@
5
5
  # to learn the current version.
6
6
  module InspecPlugins
7
7
  module Chef
8
- VERSION = "0.3.1".freeze
8
+ VERSION = "0.3.2".freeze
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inspec-chef
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Heinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-23 00:00:00.000000000 Z
11
+ date: 2020-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chef-api
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.11'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.11'
55
69
  description: This plugin allows InSpec 'inputs' to be provided by Chef Server.
56
70
  email:
57
71
  - theinen@tecracer.de