inspec-core 2.3.5 → 2.3.10
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/CHANGELOG.md +21 -8
- data/lib/bundles/inspec-compliance/api.rb +3 -353
- data/lib/bundles/inspec-compliance/configuration.rb +3 -102
- data/lib/bundles/inspec-compliance/http.rb +3 -115
- data/lib/bundles/inspec-compliance/support.rb +3 -35
- data/lib/bundles/inspec-compliance/target.rb +3 -142
- data/lib/inspec/base_cli.rb +4 -1
- data/lib/inspec/cli.rb +1 -1
- data/lib/inspec/control_eval_context.rb +2 -2
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +3 -3
- data/lib/{bundles → plugins}/inspec-compliance/README.md +0 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance.rb +12 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/api.rb +358 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/api/login.rb +192 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +266 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/configuration.rb +103 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/http.rb +116 -0
- data/lib/{bundles → plugins/inspec-compliance/lib}/inspec-compliance/images/cc-token.png +0 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/support.rb +36 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/target.rb +143 -0
- data/lib/plugins/inspec-compliance/test/functional/inspec_compliance_test.rb +43 -0
- data/lib/{bundles → plugins}/inspec-compliance/test/integration/default/cli.rb +0 -0
- data/lib/plugins/inspec-compliance/test/unit/api/login_test.rb +190 -0
- data/lib/plugins/inspec-compliance/test/unit/api_test.rb +385 -0
- data/lib/plugins/inspec-compliance/test/unit/target_test.rb +155 -0
- data/lib/resources/processes.rb +19 -3
- metadata +17 -10
- data/lib/bundles/inspec-compliance.rb +0 -16
- data/lib/bundles/inspec-compliance/.kitchen.yml +0 -20
- data/lib/bundles/inspec-compliance/api/login.rb +0 -193
- data/lib/bundles/inspec-compliance/bootstrap.sh +0 -41
- data/lib/bundles/inspec-compliance/cli.rb +0 -276
@@ -0,0 +1,192 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module InspecPlugins
|
4
|
+
module Compliance
|
5
|
+
class API
|
6
|
+
module Login
|
7
|
+
class CannotDetermineServerType < StandardError; end
|
8
|
+
|
9
|
+
def login(options)
|
10
|
+
raise ArgumentError, 'Please specify a server using `inspec compliance login https://SERVER`' unless options['server']
|
11
|
+
|
12
|
+
options['server'] = URI("https://#{options['server']}").to_s if URI(options['server']).scheme.nil?
|
13
|
+
|
14
|
+
options['server_type'] = InspecPlugins::Compliance::API.determine_server_type(options['server'], options['insecure'])
|
15
|
+
|
16
|
+
case options['server_type']
|
17
|
+
when :automate2
|
18
|
+
Login::Automate2Server.login(options)
|
19
|
+
when :automate
|
20
|
+
Login::AutomateServer.login(options)
|
21
|
+
when :compliance
|
22
|
+
Login::ComplianceServer.login(options)
|
23
|
+
else
|
24
|
+
raise CannotDetermineServerType, "Unable to determine if #{options['server']} is a Chef Automate or Chef Compliance server"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module Automate2Server
|
29
|
+
def self.login(options)
|
30
|
+
verify_thor_options(options)
|
31
|
+
|
32
|
+
options['url'] = options['server'] + '/api/v0'
|
33
|
+
token = options['dctoken'] || options['token']
|
34
|
+
store_access_token(options, token)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.store_access_token(options, token)
|
38
|
+
config = InspecPlugins::Compliance::Configuration.new
|
39
|
+
config.clean
|
40
|
+
|
41
|
+
config['automate'] = {}
|
42
|
+
config['automate']['ent'] = 'automate'
|
43
|
+
config['automate']['token_type'] = 'dctoken'
|
44
|
+
config['server'] = options['url']
|
45
|
+
config['user'] = options['user']
|
46
|
+
config['owner'] = options['user']
|
47
|
+
config['insecure'] = options['insecure'] || false
|
48
|
+
config['server_type'] = options['server_type'].to_s
|
49
|
+
config['token'] = token
|
50
|
+
config['version'] = '0'
|
51
|
+
|
52
|
+
config.store
|
53
|
+
config
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.verify_thor_options(o)
|
57
|
+
error_msg = []
|
58
|
+
|
59
|
+
error_msg.push('Please specify a user using `--user=\'USER\'`') if o['user'].nil?
|
60
|
+
|
61
|
+
if o['token'].nil? && o['dctoken'].nil?
|
62
|
+
error_msg.push('Please specify a token using `--token=\'APITOKEN\'`')
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module AutomateServer
|
70
|
+
def self.login(options)
|
71
|
+
verify_thor_options(options)
|
72
|
+
|
73
|
+
options['url'] = options['server'] + '/compliance'
|
74
|
+
token = options['dctoken'] || options['token']
|
75
|
+
store_access_token(options, token)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.store_access_token(options, token)
|
79
|
+
token_type = if options['token']
|
80
|
+
'usertoken'
|
81
|
+
else
|
82
|
+
'dctoken'
|
83
|
+
end
|
84
|
+
|
85
|
+
config = InspecPlugins::Compliance::Configuration.new
|
86
|
+
|
87
|
+
config.clean
|
88
|
+
|
89
|
+
config['automate'] = {}
|
90
|
+
config['automate']['ent'] = options['ent']
|
91
|
+
config['automate']['token_type'] = token_type
|
92
|
+
config['server'] = options['url']
|
93
|
+
config['user'] = options['user']
|
94
|
+
config['insecure'] = options['insecure'] || false
|
95
|
+
config['server_type'] = options['server_type'].to_s
|
96
|
+
config['token'] = token
|
97
|
+
config['version'] = InspecPlugins::Compliance::API.version(config)
|
98
|
+
|
99
|
+
config.store
|
100
|
+
config
|
101
|
+
end
|
102
|
+
|
103
|
+
# Automate login requires `--ent`, `--user`, and either `--token` or `--dctoken`
|
104
|
+
def self.verify_thor_options(o)
|
105
|
+
error_msg = []
|
106
|
+
|
107
|
+
error_msg.push('Please specify a user using `--user=\'USER\'`') if o['user'].nil?
|
108
|
+
error_msg.push('Please specify an enterprise using `--ent=\'automate\'`') if o['ent'].nil?
|
109
|
+
|
110
|
+
if o['token'].nil? && o['dctoken'].nil?
|
111
|
+
error_msg.push('Please specify a token using `--token=\'AUTOMATE_TOKEN\'` or `--dctoken=\'DATA_COLLECTOR_TOKEN\'`')
|
112
|
+
end
|
113
|
+
|
114
|
+
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module ComplianceServer
|
119
|
+
def self.login(options)
|
120
|
+
compliance_verify_thor_options(options)
|
121
|
+
|
122
|
+
options['url'] = options['server'] + '/api'
|
123
|
+
|
124
|
+
if options['user'] && options['token']
|
125
|
+
compliance_store_access_token(options, options['token'])
|
126
|
+
elsif options['user'] && options['password']
|
127
|
+
compliance_login_user_pass(options)
|
128
|
+
elsif options['refresh_token']
|
129
|
+
compliance_login_refresh_token(options)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.compliance_login_user_pass(options)
|
134
|
+
success, msg, token = InspecPlugins::Compliance::API.get_token_via_password(
|
135
|
+
options['url'],
|
136
|
+
options['user'],
|
137
|
+
options['password'],
|
138
|
+
options['insecure'],
|
139
|
+
)
|
140
|
+
|
141
|
+
raise msg unless success
|
142
|
+
compliance_store_access_token(options, token)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.compliance_login_refresh_token(options)
|
146
|
+
success, msg, token = InspecPlugins::Compliance::API.get_token_via_refresh_token(
|
147
|
+
options['url'],
|
148
|
+
options['refresh_token'],
|
149
|
+
options['insecure'],
|
150
|
+
)
|
151
|
+
|
152
|
+
raise msg unless success
|
153
|
+
compliance_store_access_token(options, token)
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.compliance_store_access_token(options, token)
|
157
|
+
config = InspecPlugins::Compliance::Configuration.new
|
158
|
+
config.clean
|
159
|
+
|
160
|
+
config['user'] = options['user'] if options['user']
|
161
|
+
config['server'] = options['url']
|
162
|
+
config['insecure'] = options['insecure'] || false
|
163
|
+
config['server_type'] = options['server_type'].to_s
|
164
|
+
config['token'] = token
|
165
|
+
config['version'] = InspecPlugins::Compliance::API.version(config)
|
166
|
+
|
167
|
+
config.store
|
168
|
+
config
|
169
|
+
end
|
170
|
+
|
171
|
+
# Compliance login requires `--user` or `--refresh_token`
|
172
|
+
# If `--user` then either `--password`, `--token`, or `--refresh-token`, is required
|
173
|
+
def self.compliance_verify_thor_options(o)
|
174
|
+
error_msg = []
|
175
|
+
|
176
|
+
error_msg.push('Please specify a server using `inspec compliance login https://SERVER`') if o['server'].nil?
|
177
|
+
|
178
|
+
if o['user'].nil? && o['refresh_token'].nil?
|
179
|
+
error_msg.push('Please specify a `--user=\'USER\'` or a `--refresh-token=\'TOKEN\'`')
|
180
|
+
end
|
181
|
+
|
182
|
+
if o['user'] && o['password'].nil? && o['token'].nil? && o['refresh_token'].nil?
|
183
|
+
error_msg.push('Please specify either a `--password`, `--token`, or `--refresh-token`')
|
184
|
+
end
|
185
|
+
|
186
|
+
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,266 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative 'api'
|
4
|
+
|
5
|
+
module InspecPlugins
|
6
|
+
module Compliance
|
7
|
+
class CLI < Inspec.plugin(2, :cli_command)
|
8
|
+
subcommand_desc 'compliance SUBCOMMAND', 'Chef Compliance commands'
|
9
|
+
|
10
|
+
# desc "login https://SERVER --insecure --user='USER' --ent='ENTERPRISE' --token='TOKEN'", 'Log in to a Chef Compliance/Chef Automate SERVER'
|
11
|
+
desc 'login', 'Log in to a Chef Compliance/Chef Automate SERVER'
|
12
|
+
long_desc <<-LONGDESC
|
13
|
+
`login` allows you to use InSpec with Chef Automate or a Chef Compliance Server
|
14
|
+
|
15
|
+
You need to a token for communication. More information about token retrieval
|
16
|
+
is available at:
|
17
|
+
https://docs.chef.io/api_automate.html#authentication-methods
|
18
|
+
https://docs.chef.io/api_compliance.html#obtaining-an-api-token
|
19
|
+
LONGDESC
|
20
|
+
option :insecure, aliases: :k, type: :boolean,
|
21
|
+
desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
|
22
|
+
option :user, type: :string, required: false,
|
23
|
+
desc: 'Username'
|
24
|
+
option :password, type: :string, required: false,
|
25
|
+
desc: 'Password (Chef Compliance Only)'
|
26
|
+
option :token, type: :string, required: false,
|
27
|
+
desc: 'Access token'
|
28
|
+
option :refresh_token, type: :string, required: false,
|
29
|
+
desc: 'Chef Compliance refresh token (Chef Compliance Only)'
|
30
|
+
option :dctoken, type: :string, required: false,
|
31
|
+
desc: 'Data Collector token (Chef Automate Only)'
|
32
|
+
option :ent, type: :string, required: false,
|
33
|
+
desc: 'Enterprise for Chef Automate reporting (Chef Automate Only)'
|
34
|
+
def login(server)
|
35
|
+
options['server'] = server
|
36
|
+
InspecPlugins::Compliance::API.login(options)
|
37
|
+
config = InspecPlugins::Compliance::Configuration.new
|
38
|
+
puts "Stored configuration for Chef #{config['server_type'].capitalize}: #{config['server']}' with user: '#{config['user']}'"
|
39
|
+
end
|
40
|
+
|
41
|
+
desc 'profiles', 'list all available profiles in Chef Compliance'
|
42
|
+
option :owner, type: :string, required: false,
|
43
|
+
desc: 'owner whose profiles to list'
|
44
|
+
def profiles
|
45
|
+
config = InspecPlugins::Compliance::Configuration.new
|
46
|
+
return if !loggedin(config)
|
47
|
+
|
48
|
+
# set owner to config
|
49
|
+
config['owner'] = options['owner'] || config['user']
|
50
|
+
|
51
|
+
msg, profiles = InspecPlugins::Compliance::API.profiles(config)
|
52
|
+
profiles.sort_by! { |hsh| hsh['title'] }
|
53
|
+
if !profiles.empty?
|
54
|
+
# iterate over profiles
|
55
|
+
headline('Available profiles:')
|
56
|
+
profiles.each { |profile|
|
57
|
+
owner = profile['owner_id'] || profile['owner']
|
58
|
+
li("#{profile['title']} v#{profile['version']} (#{mark_text(owner + '/' + profile['name'])})")
|
59
|
+
}
|
60
|
+
else
|
61
|
+
puts msg if msg != 'success'
|
62
|
+
puts 'Could not find any profiles'
|
63
|
+
exit 1
|
64
|
+
end
|
65
|
+
rescue InspecPlugins::Compliance::ServerConfigurationMissing
|
66
|
+
STDERR.puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
|
70
|
+
desc 'exec PROFILE', 'executes a Chef Compliance profile'
|
71
|
+
exec_options
|
72
|
+
def exec(*tests)
|
73
|
+
config = InspecPlugins::Compliance::Configuration.new
|
74
|
+
return if !loggedin(config)
|
75
|
+
o = opts(:exec).dup
|
76
|
+
diagnose(o)
|
77
|
+
configure_logger(o)
|
78
|
+
|
79
|
+
# iterate over tests and add compliance scheme
|
80
|
+
tests = tests.map { |t| 'compliance://' + InspecPlugins::Compliance::API.sanitize_profile_name(t) }
|
81
|
+
|
82
|
+
runner = Inspec::Runner.new(o)
|
83
|
+
tests.each { |target| runner.add_target(target) }
|
84
|
+
|
85
|
+
exit runner.run
|
86
|
+
rescue ArgumentError, RuntimeError, Train::UserError => e
|
87
|
+
$stderr.puts e.message
|
88
|
+
exit 1
|
89
|
+
end
|
90
|
+
|
91
|
+
desc 'download PROFILE', 'downloads a profile from Chef Compliance'
|
92
|
+
option :name, type: :string,
|
93
|
+
desc: 'Name of the archive filename (file type will be added)'
|
94
|
+
def download(profile_name)
|
95
|
+
o = options.dup
|
96
|
+
configure_logger(o)
|
97
|
+
|
98
|
+
config = InspecPlugins::Compliance::Configuration.new
|
99
|
+
return if !loggedin(config)
|
100
|
+
|
101
|
+
profile_name = InspecPlugins::Compliance::API.sanitize_profile_name(profile_name)
|
102
|
+
if InspecPlugins::Compliance::API.exist?(config, profile_name)
|
103
|
+
puts "Downloading `#{profile_name}`"
|
104
|
+
|
105
|
+
fetcher = InspecPlugins::Compliance::Fetcher.resolve(
|
106
|
+
{
|
107
|
+
compliance: profile_name,
|
108
|
+
},
|
109
|
+
)
|
110
|
+
|
111
|
+
# we provide a name, the fetcher adds the extension
|
112
|
+
_owner, id = profile_name.split('/')
|
113
|
+
file_name = fetcher.fetch(o.name || id)
|
114
|
+
puts "Profile stored to #{file_name}"
|
115
|
+
else
|
116
|
+
puts "Profile #{profile_name} is not available in Chef Compliance."
|
117
|
+
exit 1
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
desc 'upload PATH', 'uploads a local profile to Chef Compliance'
|
122
|
+
option :overwrite, type: :boolean, default: false,
|
123
|
+
desc: 'Overwrite existing profile on Server.'
|
124
|
+
option :owner, type: :string, required: false,
|
125
|
+
desc: 'Owner that should own the profile'
|
126
|
+
def upload(path) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, PerceivedComplexity, Metrics/CyclomaticComplexity
|
127
|
+
config = InspecPlugins::Compliance::Configuration.new
|
128
|
+
return if !loggedin(config)
|
129
|
+
|
130
|
+
# set owner to config
|
131
|
+
config['owner'] = options['owner'] || config['user']
|
132
|
+
|
133
|
+
unless File.exist?(path)
|
134
|
+
puts "Directory #{path} does not exist."
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
|
138
|
+
vendor_deps(path, options) if File.directory?(path)
|
139
|
+
|
140
|
+
o = options.dup
|
141
|
+
configure_logger(o)
|
142
|
+
|
143
|
+
# only run against the mock backend, otherwise we run against the local system
|
144
|
+
o[:backend] = Inspec::Backend.create(target: 'mock://')
|
145
|
+
o[:check_mode] = true
|
146
|
+
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
|
147
|
+
|
148
|
+
# check the profile, we only allow to upload valid profiles
|
149
|
+
profile = Inspec::Profile.for_target(path, o)
|
150
|
+
|
151
|
+
# start verification process
|
152
|
+
error_count = 0
|
153
|
+
error = lambda { |msg|
|
154
|
+
error_count += 1
|
155
|
+
puts msg
|
156
|
+
}
|
157
|
+
|
158
|
+
result = profile.check
|
159
|
+
unless result[:summary][:valid]
|
160
|
+
error.call('Profile check failed. Please fix the profile before upload.')
|
161
|
+
else
|
162
|
+
puts('Profile is valid')
|
163
|
+
end
|
164
|
+
|
165
|
+
# determine user information
|
166
|
+
if (config['token'].nil? && config['refresh_token'].nil?) || config['user'].nil?
|
167
|
+
error.call('Please login via `inspec compliance login`')
|
168
|
+
end
|
169
|
+
|
170
|
+
# read profile name from inspec.yml
|
171
|
+
profile_name = profile.params[:name]
|
172
|
+
|
173
|
+
# read profile version from inspec.yml
|
174
|
+
profile_version = profile.params[:version]
|
175
|
+
|
176
|
+
# check that the profile is not uploaded already,
|
177
|
+
# confirm upload to the user (overwrite with --force)
|
178
|
+
if InspecPlugins::Compliance::API.exist?(config, "#{config['owner']}/#{profile_name}##{profile_version}") && !options['overwrite']
|
179
|
+
error.call('Profile exists on the server, use --overwrite')
|
180
|
+
end
|
181
|
+
|
182
|
+
# abort if we found an error
|
183
|
+
if error_count > 0
|
184
|
+
puts "Found #{error_count} error(s)"
|
185
|
+
exit 1
|
186
|
+
end
|
187
|
+
|
188
|
+
# if it is a directory, tar it to tmp directory
|
189
|
+
generated = false
|
190
|
+
if File.directory?(path)
|
191
|
+
generated = true
|
192
|
+
archive_path = Dir::Tmpname.create([profile_name, '.tar.gz']) {}
|
193
|
+
puts "Generate temporary profile archive at #{archive_path}"
|
194
|
+
profile.archive({ output: archive_path, ignore_errors: false, overwrite: true })
|
195
|
+
else
|
196
|
+
archive_path = path
|
197
|
+
end
|
198
|
+
|
199
|
+
puts "Start upload to #{config['owner']}/#{profile_name}"
|
200
|
+
pname = ERB::Util.url_encode(profile_name)
|
201
|
+
|
202
|
+
if InspecPlugins::Compliance::API.is_automate_server?(config) || InspecPlugins::Compliance::API.is_automate2_server?(config)
|
203
|
+
puts 'Uploading to Chef Automate'
|
204
|
+
else
|
205
|
+
puts 'Uploading to Chef Compliance'
|
206
|
+
end
|
207
|
+
success, msg = InspecPlugins::Compliance::API.upload(config, config['owner'], pname, archive_path)
|
208
|
+
|
209
|
+
# delete temp file if it was temporary generated
|
210
|
+
File.delete(archive_path) if generated && File.exist?(archive_path)
|
211
|
+
|
212
|
+
if success
|
213
|
+
puts 'Successfully uploaded profile'
|
214
|
+
else
|
215
|
+
puts 'Error during profile upload:'
|
216
|
+
puts msg
|
217
|
+
exit 1
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
desc 'version', 'displays the version of the Chef Compliance server'
|
222
|
+
def version
|
223
|
+
config = InspecPlugins::Compliance::Configuration.new
|
224
|
+
info = InspecPlugins::Compliance::API.version(config)
|
225
|
+
if !info.nil? && info['version']
|
226
|
+
puts "Name: #{info['api']}"
|
227
|
+
puts "Version: #{info['version']}"
|
228
|
+
else
|
229
|
+
puts 'Could not determine server version.'
|
230
|
+
exit 1
|
231
|
+
end
|
232
|
+
rescue InspecPlugins::Compliance::ServerConfigurationMissing
|
233
|
+
puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
|
234
|
+
exit 1
|
235
|
+
end
|
236
|
+
|
237
|
+
desc 'logout', 'user logout from Chef Compliance'
|
238
|
+
def logout
|
239
|
+
config = InspecPlugins::Compliance::Configuration.new
|
240
|
+
unless config.supported?(:oidc) || config['token'].nil? || config['server_type'] == 'automate'
|
241
|
+
config = InspecPlugins::Compliance::Configuration.new
|
242
|
+
url = "#{config['server']}/logout"
|
243
|
+
InspecPlugins::Compliance::HTTP.post(url, config['token'], config['insecure'], !config.supported?(:oidc))
|
244
|
+
end
|
245
|
+
success = config.destroy
|
246
|
+
|
247
|
+
if success
|
248
|
+
puts 'Successfully logged out'
|
249
|
+
else
|
250
|
+
puts 'Could not log out'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def loggedin(config)
|
257
|
+
serverknown = !config['server'].nil?
|
258
|
+
puts 'You need to login first with `inspec compliance login`' if !serverknown
|
259
|
+
serverknown
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# register the subcommand to Inspec CLI registry
|
264
|
+
# Inspec::Plugins::CLI.add_subcommand(InspecPlugins::ComplianceCLI, 'compliance', 'compliance SUBCOMMAND ...', 'Chef InspecPlugins::Compliance commands', {})
|
265
|
+
end
|
266
|
+
end
|