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 +4 -4
- data/Gemfile +1 -0
- data/inspec-chef.gemspec +1 -0
- data/lib/inspec-chef/input.rb +180 -148
- data/lib/inspec-chef/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 29c070e9e924069b6c2f7418bdfd8164b0c69f9ae3ad7447928b3cdb99ca6e30
|
4
|
+
data.tar.gz: 875794f04d148b86a41a401c481bc0fb53e6b398059668fc1d307820e4b285ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e87dee841335d6e05a96cdb2d74bd8a77b7b975d9a54f73a0eae94766cb7def48adc20291ea821d0b3d51096b9cb4b5ad1419d8105f9f253a784d22f06b42f02
|
7
|
+
data.tar.gz: 47360a9eb493ddf659535f7f59b3810bd271a2ba49b3e84677e3cc4c6e936b7273eb764d211fc7d8d4ad6272cc2a027311f46c4690eb7aabd4cff03637a8fcd2
|
data/Gemfile
CHANGED
data/inspec-chef.gemspec
CHANGED
data/lib/inspec-chef/input.rb
CHANGED
@@ -3,191 +3,223 @@ require "jmespath"
|
|
3
3
|
require "resolv"
|
4
4
|
require "uri"
|
5
5
|
|
6
|
-
module InspecPlugins
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
22
|
-
|
23
|
-
return nil unless valid_plugin_input? input_uri
|
16
|
+
# ========================================================================
|
17
|
+
# Dependency Injection
|
24
18
|
|
25
|
-
|
19
|
+
attr_writer :inspec_config, :logger
|
26
20
|
|
27
|
-
|
28
|
-
|
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
|
-
|
35
|
-
|
25
|
+
def logger
|
26
|
+
@logger ||= Inspec::Log
|
27
|
+
end
|
36
28
|
|
37
|
-
|
38
|
-
|
29
|
+
# ========================================================================
|
30
|
+
# Input Plugin API
|
39
31
|
|
40
|
-
|
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
|
-
|
43
|
-
def inside_testkitchen?
|
44
|
-
!! defined?(::Kitchen::Logger)
|
45
|
-
end
|
36
|
+
connect_to_chef_server
|
46
37
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
51
|
+
result
|
52
|
+
end
|
68
53
|
|
69
|
-
|
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
|
-
|
77
|
-
|
78
|
-
target = Inspec::Config.cached.final_options["target"]
|
79
|
-
URI.parse(target)&.host
|
80
|
-
end
|
56
|
+
# ========================================================================
|
57
|
+
# Helper Methods
|
81
58
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
163
|
+
# Return if connection is established
|
164
|
+
def server_connected?
|
165
|
+
! chef_server.nil?
|
166
|
+
end
|
137
167
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
#
|
160
|
-
|
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
|
-
|
163
|
-
|
198
|
+
merge_attributes(data)
|
199
|
+
else
|
200
|
+
kitchen_provisioner_config[:attributes]
|
201
|
+
end
|
202
|
+
end
|
164
203
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
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
|
-
|
181
|
-
|
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
|
data/lib/inspec-chef/version.rb
CHANGED
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.
|
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-
|
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
|