inspec-chef 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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