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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -8
  3. data/lib/bundles/inspec-compliance/api.rb +3 -353
  4. data/lib/bundles/inspec-compliance/configuration.rb +3 -102
  5. data/lib/bundles/inspec-compliance/http.rb +3 -115
  6. data/lib/bundles/inspec-compliance/support.rb +3 -35
  7. data/lib/bundles/inspec-compliance/target.rb +3 -142
  8. data/lib/inspec/base_cli.rb +4 -1
  9. data/lib/inspec/cli.rb +1 -1
  10. data/lib/inspec/control_eval_context.rb +2 -2
  11. data/lib/inspec/version.rb +1 -1
  12. data/lib/matchers/matchers.rb +3 -3
  13. data/lib/{bundles → plugins}/inspec-compliance/README.md +0 -0
  14. data/lib/plugins/inspec-compliance/lib/inspec-compliance.rb +12 -0
  15. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api.rb +358 -0
  16. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api/login.rb +192 -0
  17. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +266 -0
  18. data/lib/plugins/inspec-compliance/lib/inspec-compliance/configuration.rb +103 -0
  19. data/lib/plugins/inspec-compliance/lib/inspec-compliance/http.rb +116 -0
  20. data/lib/{bundles → plugins/inspec-compliance/lib}/inspec-compliance/images/cc-token.png +0 -0
  21. data/lib/plugins/inspec-compliance/lib/inspec-compliance/support.rb +36 -0
  22. data/lib/plugins/inspec-compliance/lib/inspec-compliance/target.rb +143 -0
  23. data/lib/plugins/inspec-compliance/test/functional/inspec_compliance_test.rb +43 -0
  24. data/lib/{bundles → plugins}/inspec-compliance/test/integration/default/cli.rb +0 -0
  25. data/lib/plugins/inspec-compliance/test/unit/api/login_test.rb +190 -0
  26. data/lib/plugins/inspec-compliance/test/unit/api_test.rb +385 -0
  27. data/lib/plugins/inspec-compliance/test/unit/target_test.rb +155 -0
  28. data/lib/resources/processes.rb +19 -3
  29. metadata +17 -10
  30. data/lib/bundles/inspec-compliance.rb +0 -16
  31. data/lib/bundles/inspec-compliance/.kitchen.yml +0 -20
  32. data/lib/bundles/inspec-compliance/api/login.rb +0 -193
  33. data/lib/bundles/inspec-compliance/bootstrap.sh +0 -41
  34. 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