inspec 2.3.5 → 2.3.10

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