openstack_taster 1.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d197f40b701de60109b9171f250962922dc2f860
4
+ data.tar.gz: 70c9e7da35b886369ae89a4dece5b1e50260235a
5
+ SHA512:
6
+ metadata.gz: d2868e2dae6f66633d73f6ddb54edf80b905e3c35b6b08121128c77729ce597a5e64d7f76c08d1f26e5cb56f9140499e76de3e44ea2e8904b03693e903bbf455
7
+ data.tar.gz: f391b2d1d487bd2e4fedd8ba9aaa49f7599b3769ff71d0c120fdb9f93b9a63426deb8c8da841bee5d302cb1636c9d080f13bdee7e684bf7f70bcdda343744901
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+
6
+ suites = {
7
+ 'security' => 'Runs the security test suite',
8
+ 'volumes' => 'Runs the volume test suite'
9
+ }
10
+ settings = {}
11
+
12
+ ARGV << '-h' if ARGV.empty?
13
+
14
+ parser = OptionParser.new do |opts|
15
+ opts.banner = "Usage: openstack_taster <image_name> {suite_name} [--create-snapshot]\n" \
16
+ " or: openstack_taster <image_name> [--create-snapshot]\n" \
17
+ ' or: openstack_taster <option>'
18
+ opts.separator('')
19
+ opts.separator('Available Arguments:')
20
+ opts.on('-c', '--create-snapshot', 'Create snapshot upon test failure.') do
21
+ settings[:create_snapshot] = true
22
+ end
23
+ opts.on('-h', '--help', 'Print usage information.') do
24
+ puts opts
25
+ settings[:exit] = true
26
+ end
27
+ opts.separator('')
28
+ opts.separator('Test Suites:')
29
+ suites.each do |suite, desc|
30
+ opts.separator(" #{suite}\t\t#{desc}")
31
+ end
32
+ end
33
+
34
+ begin
35
+ params = parser.parse!
36
+ rescue OptionParser::InvalidOption => io
37
+ puts io.message
38
+ puts
39
+ puts parser
40
+ exit 1
41
+ rescue StandardError => e
42
+ puts 'Argument parsing failed:'
43
+ puts e.message
44
+ puts e.backtrace
45
+ exit 1
46
+ end
47
+
48
+ exit if settings[:exit] # exit inside OptionParser causes problems.
49
+
50
+ begin
51
+ case params.length
52
+ when 1
53
+ image_name = params[0]
54
+ suites.each_key { |suite| settings[suite.to_sym] = true }
55
+ when 2
56
+ raise "#{params[1]} is not a test suite!" unless suites.include? params[1]
57
+ image_name = params[0]
58
+ settings[params[1].to_sym] = true
59
+ else
60
+ raise 'Incorrect format!'
61
+ end
62
+ rescue StandardError => e
63
+ puts e.message
64
+ puts
65
+ puts parser
66
+ exit(1)
67
+ end
68
+
69
+ require 'fog/openstack'
70
+ require 'openstack_taster'
71
+
72
+ auth_url = String.new(ENV['OS_AUTH_URL'])
73
+ auth_url << '/tokens' unless auth_url.end_with?('tokens')
74
+ auth_url.freeze
75
+
76
+ OPENSTACK_CREDS = {
77
+ openstack_auth_url: auth_url,
78
+ openstack_username: ENV['OS_USERNAME'],
79
+ openstack_tenant: ENV['OS_TENANT_NAME'],
80
+ openstack_api_key: ENV['OS_PASSWORD']
81
+ }.freeze
82
+
83
+ SSH_KEYS = {
84
+ keypair: ENV['OS_SSH_KEYPAIR'],
85
+ private_key: ENV['OS_PRIVATE_SSH_KEY'],
86
+ public_key: ENV['OS_PUBLIC_SSH_KEY'] # REVIEW
87
+ }.freeze
88
+
89
+ controller_host = auth_url.split(':')[1].delete('//')
90
+ LOG_DIR = "logs/#{controller_host}"
91
+
92
+ compute = Fog::Compute::OpenStack.new(OPENSTACK_CREDS)
93
+ volume = Fog::Volume::OpenStack.new(OPENSTACK_CREDS)
94
+ image = Fog::Image::OpenStack.new(OPENSTACK_CREDS)
95
+ network = Fog::Network::OpenStack.new(OPENSTACK_CREDS)
96
+
97
+ exit OpenStackTaster.new(
98
+ compute, volume, image, network,
99
+ SSH_KEYS, LOG_DIR
100
+ ).taste(image_name, settings)
@@ -0,0 +1,431 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'date'
5
+ require 'excon'
6
+ require 'net/ssh'
7
+ require 'pry'
8
+ require 'inspec'
9
+
10
+ # @author Andrew Tolvstad, Samarendra Hedaoo, Cody Holliday
11
+ class OpenStackTaster
12
+ INSTANCE_FLAVOR_NAME = 'm1.small'
13
+ INSTANCE_NETWORK_NAME = 'public'
14
+ INSTANCE_NAME_PREFIX = 'taster'
15
+ INSTANCE_VOLUME_MOUNT_POINT = '/mnt/taster_volume'
16
+
17
+ VOLUME_TEST_FILE_NAME = 'info'
18
+ VOLUME_TEST_FILE_CONTENTS = nil # Contents would be something like 'test-vol-1 on openpower8.osuosl.bak'
19
+ TIMEOUT_INSTANCE_CREATE = 20
20
+ TIMEOUT_VOLUME_ATTACH = 10
21
+ TIMEOUT_VOLUME_PERSIST = 20
22
+ TIMEOUT_INSTANCE_TO_BE_CREATED = 20
23
+ TIMEOUT_INSTANCE_STARTUP = 30
24
+ TIMEOUT_SSH_RETRY = 15
25
+
26
+ MAX_SSH_RETRY = 3
27
+
28
+ TIME_SLUG_FORMAT = '%Y%m%d_%H%M%S'
29
+
30
+ def initialize(
31
+ compute_service,
32
+ volume_service,
33
+ image_service,
34
+ network_service,
35
+ ssh_keys,
36
+ log_dir
37
+ )
38
+ @compute_service = compute_service
39
+ @volume_service = volume_service
40
+ @image_service = image_service
41
+ @network_service = network_service
42
+
43
+ @volumes = @volume_service.volumes
44
+
45
+ @ssh_keypair = ssh_keys[:keypair]
46
+ @ssh_private_key = ssh_keys[:private_key]
47
+ @ssh_public_key = ssh_keys[:public_key] # REVIEW
48
+
49
+ @session_id = object_id
50
+ @log_dir = log_dir + "/#{@session_id}"
51
+
52
+ @instance_flavor = @compute_service.flavors
53
+ .select { |flavor| flavor.name == INSTANCE_FLAVOR_NAME }.first
54
+ @instance_network = @network_service.networks
55
+ .select { |network| network.name == INSTANCE_NETWORK_NAME }.first
56
+ end
57
+
58
+ # Taste a specified image
59
+ # @param image_name [String] The name on OpenStack of the image to be tested.
60
+ # @param settings [Hash] A hash of settings to enable and disable tests, snapshot creation upon failure.
61
+ # @return [Boolean] success or failure of tests on image.
62
+ # @note The testing section could be further streamlined by:
63
+ # creating a naming standard for test functions (i.e. taste_<name>)
64
+ # limiting the parameters of each test to be: instance, distro_username
65
+ # Adding a 'suites' subhash to the settings hash
66
+ # Then that subhash can be iterated over, use eval to call each function,
67
+ # appending the suite name to 'taste_' for the function name
68
+ # and passing the standardized parameters
69
+ # @todo Reduce Percieved and Cyclomatic complexity
70
+ # @todo Images over compute service is deprecated
71
+ def taste(image_name, settings)
72
+ image = @compute_service.images
73
+ .select { |i| i.name == image_name }.first
74
+
75
+ abort("#{image_name} is not an available image.") if image.nil?
76
+
77
+ distro_user_name = image.name.downcase.gsub(/[^a-z].*$/, '') # truncate downcased name at first non-alpha char
78
+ distro_arch = image.name.downcase.slice(-2, 2)
79
+ instance_name = format(
80
+ '%s-%s-%s-%s',
81
+ INSTANCE_NAME_PREFIX,
82
+ Time.new.strftime(TIME_SLUG_FORMAT),
83
+ distro_user_name,
84
+ distro_arch
85
+ )
86
+
87
+ FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
88
+
89
+ instance_logger = Logger.new("#{@log_dir}/#{instance_name}.log")
90
+
91
+ error_log(
92
+ instance_logger,
93
+ 'info',
94
+ "Tasting #{image.name} as '#{instance_name}' with username '#{distro_user_name}'.\nBuilding...",
95
+ true
96
+ )
97
+
98
+ instance = @compute_service.servers.create(
99
+ name: instance_name,
100
+ flavor_ref: @instance_flavor.id,
101
+ image_ref: image.id,
102
+ nics: [{ net_id: @instance_network.id }],
103
+ key_name: @ssh_keypair
104
+ )
105
+
106
+ if instance.nil?
107
+ error_log(instance_logger, 'error', 'Failed to create instance.', true)
108
+ return false
109
+ end
110
+
111
+ instance.class.send(:attr_accessor, 'logger')
112
+
113
+ instance.logger = instance_logger
114
+
115
+ instance.wait_for(TIMEOUT_INSTANCE_TO_BE_CREATED) { ready? }
116
+
117
+ error_log(instance.logger, 'info', "Sleeping #{TIMEOUT_INSTANCE_STARTUP} seconds for OS startup...", true)
118
+ sleep TIMEOUT_INSTANCE_STARTUP
119
+
120
+ error_log(instance.logger, 'info', "Testing for instance '#{instance.id}'.", true)
121
+
122
+ # Run tests
123
+ return_values = []
124
+ return_values.push taste_security(instance, distro_user_name) if settings[:security]
125
+ return_values.push taste_volumes(instance, distro_user_name) if settings[:volumes]
126
+
127
+ if settings[:create_snapshot] && !return_values.all?
128
+ error_log(instance.logger, 'info', "Tests failed for instance '#{instance.id}'. Creating image...", true)
129
+ create_image(instance) # Create image here since it is destroyed before scope returns to taste function
130
+ end
131
+ return return_values.all?
132
+ rescue Fog::Errors::TimeoutError
133
+ puts 'Instance creation timed out.'
134
+ error_log(instance.logger, 'error', "Instance fault: #{instance.fault}")
135
+ return false
136
+ rescue Interrupt
137
+ puts "\nCaught interrupt"
138
+ puts "Exiting session #{@session_id}"
139
+ raise
140
+ ensure
141
+ if instance
142
+ puts "Destroying instance for session #{@session_id}.\n\n"
143
+ instance.destroy
144
+ end
145
+ end
146
+
147
+ # Runs the security test suite using inspec
148
+ # @param instance [Fog::Image::OpenStack::Image] The instance to test.
149
+ # @param username [String] The username to use when logging into the instance.
150
+ # @return [Boolean] Whether or not the image passed hte security tests.
151
+ # @todo Don't crash when connection refused.
152
+ def taste_security(instance, username)
153
+ opts = {
154
+ 'backend' => 'ssh',
155
+ 'host' => instance.addresses['public'].first['addr'],
156
+ 'port' => 22,
157
+ 'user' => username,
158
+ 'keys_only' => true,
159
+ 'key_files' => @ssh_private_key,
160
+ 'logger' => instance.logger
161
+ }
162
+
163
+ tries = 0
164
+
165
+ begin
166
+ runner = Inspec::Runner.new(opts)
167
+ runner.add_target(File.dirname(__FILE__) + '/../tests')
168
+ runner.run
169
+ rescue RuntimeError => e
170
+ puts "Encountered error \"#{e.message}\" while testing the instance."
171
+ if tries < MAX_SSH_RETRY
172
+ tries += 1
173
+ puts "Initiating SSH attempt #{tries} in #{TIMEOUT_SSH_RETRY} seconds"
174
+ sleep TIMEOUT_SSH_RETRY
175
+ retry
176
+ end
177
+ error_log(instance.logger, 'error', e.backtrace, false, 'Inspec Runner')
178
+ error_log(instance.logger, 'error', e.message, false, 'Inspec Runner')
179
+ return true
180
+ rescue StandardError => e
181
+ puts "Encountered error \"#{e.message}\". Aborting test."
182
+ return true
183
+ end
184
+
185
+ error_log(
186
+ instance.logger,
187
+ 'info',
188
+ "Inspec Test Results\n" +
189
+ runner.report[:controls].map do |test|
190
+ "#{test[:status].upcase}: #{test[:code_desc]}\n#{test[:message]}"
191
+ end.join("\n")
192
+ )
193
+
194
+ if runner.report[:controls].any? { |test| test[:status] == 'failed' }
195
+ error_log(instance.logger, 'warn', 'Image failed security test suite')
196
+ return false
197
+ end
198
+ true
199
+ end
200
+
201
+ # Write an error message to the log and optionally stdout.
202
+ # @param logger [Logger] the logger used to record the message.
203
+ # @param level [String] the level to use when logging.
204
+ # @param message [String] the message to write
205
+ # @param dup_stdout [Boolean] whether or not to print the message to stdout
206
+ # @param context [String] the context of the message to be logged. i.e. SSH, Inspec, etc.
207
+ def error_log(logger, level, message, dup_stdout = false, context = nil)
208
+ puts message if dup_stdout
209
+
210
+ begin
211
+ logger.add(Logger.const_get(level.upcase), message, context)
212
+ rescue NameError
213
+ puts
214
+ puts "\e[31m#{level} is not a severity. Make sure that you use the correct string for logging severity!\e[0m"
215
+ puts
216
+ logger.error('Taster Source Code') { "#{level} is not a logging severity name. Defaulting to INFO." }
217
+ logger.info(context) { message }
218
+ end
219
+ end
220
+
221
+ # Get the name of the image from which an instance was created.
222
+ # @param instance [Fog::Compute::OpenStack::Server] the instance to query
223
+ # @return [String] the name of the image
224
+ def get_image_name(instance)
225
+ @image_service
226
+ .get_image_by_id(instance.image['id'])
227
+ .body['name']
228
+ end
229
+
230
+ # Create an image of an instance.
231
+ # @note This method blocks until snapshot creation is complete on the server.
232
+ # @param instance [Fog::Compute::OpenStack::Server] the instance to query
233
+ # @return [Fog::Image::OpenStack::Image] the generated image
234
+ def create_image(instance)
235
+ image_name = [
236
+ instance.name,
237
+ get_image_name(instance)
238
+ ].join('_')
239
+
240
+ response = instance.create_image(image_name)
241
+ image_id = response.body['image']['id']
242
+
243
+ @image_service.images
244
+ .find_by_id(image_id)
245
+ .wait_for { status == 'active' }
246
+ end
247
+
248
+ # Run the set of tests for each available volume on an instance.
249
+ # @param instance [Fog::Compute::OpenStack::Server] the instance to query
250
+ # @param username [String] the username to use when logging into the instance
251
+ # @return [Boolean] Whether or not the tests succeeded
252
+ def taste_volumes(instance, username)
253
+ mount_failures = @volumes.reject do |volume|
254
+ if volume.attachments.any?
255
+ error_log(instance.logger, 'info', "Volume '#{volume.name}' is already in an attached state; skipping.", true)
256
+ next
257
+ end
258
+
259
+ unless volume_attach?(instance, volume)
260
+ error_log(instance.logger, 'error', "Volume '#{volume.name}' failed to attach.", true)
261
+ next
262
+ end
263
+
264
+ volume_mount_unmount?(instance, username, volume)
265
+ end
266
+
267
+ detach_failures = @volumes.reject do |volume|
268
+ volume_detach?(instance, volume)
269
+ end
270
+
271
+ if mount_failures.empty? && detach_failures.empty?
272
+ error_log(instance.logger, 'info', "\nEncountered 0 failures.", true)
273
+ true
274
+ else
275
+ error_log(
276
+ instance.logger,
277
+ 'error',
278
+ "\nEncountered #{mount_failures.count} mount failures and #{detach_failures.count} detach failures.",
279
+ true
280
+ )
281
+ error_log(instance.logger, 'error', "\nEncountered failures.", true)
282
+ false
283
+ end
284
+ end
285
+
286
+ # A helper method to execute a series of commands remotely on an instance. This helper
287
+ # passes its block directly to `Net::SSH#start()`.
288
+ # @param instance [Fog::Compute::OpenStack::Server] the instance on which to run the commands
289
+ # @param username [String] the username to use when logging into the instance
290
+ # @todo Don't crash when connection refused.
291
+ def with_ssh(instance, username, &block)
292
+ tries = 0
293
+ instance.logger.progname = 'SSH'
294
+ begin
295
+ Net::SSH.start(
296
+ instance.addresses['public'].first['addr'],
297
+ username,
298
+ verbose: :info,
299
+ paranoid: false,
300
+ logger: instance.logger,
301
+ keys: [@ssh_private_key],
302
+ &block
303
+ )
304
+ rescue Errno::ECONNREFUSED => e
305
+ puts "Encountered #{e.message} while connecting to the instance."
306
+ if tries < MAX_SSH_RETRY
307
+ tries += 1
308
+ puts "Initiating SSH attempt #{tries} in #{TIMEOUT_SSH_RETRY} seconds"
309
+ sleep TIMEOUT_SSH_RETRY
310
+ retry
311
+ end
312
+ error_log(instance.logger, 'error', e.backtrace, false, 'SSH')
313
+ error_log(instance.logger, 'error', e.message, false, 'SSH')
314
+ exit 1
315
+ end
316
+ end
317
+
318
+ # Test volume attachment for a given instance and volume.
319
+ # @param instance [Fog::Compute::OpenStack::Server] the instance to which to attach the volume
320
+ # @param volume [Fog::Volume::OpenStack::Volume] the volume to attach
321
+ # @return [Boolean] whether or not the attachment was successful
322
+ def volume_attach?(instance, volume)
323
+ volume_attached = lambda do |_|
324
+ volume_attachments.any? do |attachment|
325
+ attachment['volumeId'] == volume.id
326
+ end
327
+ end
328
+
329
+ error_log(instance.logger, 'info', "Attaching volume '#{volume.name}' (#{volume.id})...", true)
330
+ @compute_service.attach_volume(volume.id, instance.id, nil)
331
+ instance.wait_for(TIMEOUT_VOLUME_ATTACH, &volume_attached)
332
+
333
+ error_log(instance.logger, 'info', "Sleeping #{TIMEOUT_VOLUME_PERSIST} seconds for attachment persistance...", true)
334
+ sleep TIMEOUT_VOLUME_PERSIST
335
+
336
+ # In the off chance that the volume host goes down, catch it.
337
+ if instance.instance_eval(&volume_attached)
338
+ return true if volume.reload.attachments.first
339
+ error_log(instance.logger, 'error', "Failed to attach '#{volume.name}': Volume host might be down.", true)
340
+ else
341
+ error_log(instance.logger, 'error', "Failed to attach '#{volume.name}': Volume was unexpectedly detached.", true)
342
+ end
343
+
344
+ false
345
+ rescue Excon::Error => e
346
+ puts 'Error attaching volume, check log for details.'
347
+ error_log(instance.logger, 'error', e.message)
348
+ false
349
+ rescue Fog::Errors::TimeoutError
350
+ error_log(instance.logger, 'error', "Failed to attach '#{volume.name}': Operation timed out.", true)
351
+ false
352
+ end
353
+
354
+ # Test volume mounting and unmounting for an instance and a volume.
355
+ # @param instance [Fog::Compute::OpenStack::Server] the instance on which to mount the volume
356
+ # @param username [String] the username to use when logging into the instance
357
+ # @param volume [Fog::Volume::OpenStack::Volume] the volume to mount
358
+ # @return [Boolean] whether or not the mounting/unmounting was successful
359
+ def volume_mount_unmount?(instance, username, volume)
360
+ mount = INSTANCE_VOLUME_MOUNT_POINT
361
+ file_name = VOLUME_TEST_FILE_NAME
362
+ file_contents = VOLUME_TEST_FILE_CONTENTS
363
+ vdev = @volume_service.volumes.find_by_id(volume.id)
364
+ .attachments.first['device']
365
+ vdev << '1'
366
+
367
+ log_partitions(instance, username)
368
+
369
+ commands = [
370
+ ["echo -e \"127.0.0.1\t$HOSTNAME\" | sudo tee -a /etc/hosts", nil], # to fix problems with sudo and DNS resolution
371
+ ['sudo partprobe -s', nil],
372
+ ["[ -d '#{mount}' ] || sudo mkdir #{mount}", ''],
373
+ ["sudo mount #{vdev} #{mount}", ''],
374
+ ["sudo cat #{mount}/#{file_name}", file_contents],
375
+ ["sudo umount #{mount}", '']
376
+ ]
377
+
378
+ error_log(instance.logger, 'info', "Mounting volume '#{volume.name}' (#{volume.id})...", true)
379
+
380
+ error_log(instance.logger, 'info', 'Mounting from inside the instance...', true)
381
+ with_ssh(instance, username) do |ssh|
382
+ commands.each do |command, expected|
383
+ result = ssh.exec!(command).chomp
384
+ if expected.nil?
385
+ error_log(instance.logger, 'info', "#{command} yielded '#{result}'")
386
+ elsif result != expected
387
+ error_log(
388
+ instance.logger,
389
+ 'error',
390
+ "Failure while running '#{command}':\n\texpected '#{expected}'\n\tgot '#{result}'",
391
+ true
392
+ )
393
+ return false # returns from volume_mount_unmount?
394
+ end
395
+ end
396
+ end
397
+ true
398
+ end
399
+
400
+ # Log instance's partition listing.
401
+ # @param instance [Fog::Compute::OpenStack::Server] the instance to log
402
+ # @param username [String] the username to use when logging in to the instance
403
+ def log_partitions(instance, username)
404
+ puts 'Logging partition list and dmesg...'
405
+
406
+ record_info_commands = [
407
+ 'cat /proc/partitions',
408
+ 'dmesg | tail -n 20'
409
+ ]
410
+
411
+ with_ssh(instance, username) do |ssh|
412
+ record_info_commands.each do |command|
413
+ result = ssh.exec!(command)
414
+ error_log(instance.logger, 'info', "Ran '#{command}' and got '#{result}'")
415
+ end
416
+ end
417
+ end
418
+
419
+ # Detach volume from instance.
420
+ # @param instance [Fog::Compute::OpenStack::Server] the instance from which to detach
421
+ # @param volume [Fog::Volume::OpenStack::Volume] the volume to detach
422
+ # @return [Boolean] whether or not the detachment succeeded
423
+ def volume_detach?(instance, volume)
424
+ error_log(instance.logger, 'info', "Detaching #{volume.name}.", true)
425
+ instance.detach_volume(volume.id)
426
+ rescue Excon::Error => e
427
+ puts 'Failed to detach. check log for details.'
428
+ error_log(instance.logger, 'error', e.message)
429
+ false
430
+ end
431
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ control 'security-1.0' do
3
+ impact 1.0
4
+ title 'Openstack Image Security Test'
5
+ desc 'Tests the security of images used for Openstack.'
6
+
7
+ username = user.username
8
+
9
+ describe sshd_config do
10
+ its('PermitRootLogin') { should eq 'no' }
11
+ its('PasswordAuthentication') { should eq 'no' }
12
+ its('ChallengeResponseAuthentication') { should eq 'no' }
13
+ its('KbdInteractiveAuthentication') { should eq 'no' }
14
+ end
15
+
16
+ describe 'running sshd config' do
17
+ let(:resource) { command('sudo sshd -T') }
18
+
19
+ it 'should not permit root login' do
20
+ expect(resource.stdout).to cmp(/^PermitRootLogin no/i)
21
+ end
22
+
23
+ it 'should not permit password authentication' do
24
+ expect(resource.stdout).to cmp(/^PasswordAuthentication no/i)
25
+ end
26
+
27
+ it 'should not permit challenge response authentication' do
28
+ expect(resource.stdout).to cmp(/^ChallengeResponseAuthentication no/i)
29
+ end
30
+ it 'should not permit keyboard interactive authentication' do
31
+ expect(resource.stdout).to cmp(/^KbdInteractiveAuthentication no/i)
32
+ end
33
+ end
34
+
35
+ # Our version of inspec does not give us a warning about the list matcher,
36
+ # but in version 2.0 of inspec this will be removed.
37
+ # This tests the number of instances of sshd on the system.
38
+ describe processes('sshd') do
39
+ its('list.length') { should eq 1 }
40
+ end
41
+
42
+ describe.one do
43
+ describe user(username) do
44
+ its('groups') { should eq %w(root wheel sudo) }
45
+ end
46
+
47
+ describe command('sudo -U ' + username + ' -l') do
48
+ its('stdout') { should cmp(/\(ALL\) ((NO)*PASSWD)*: ALL/) }
49
+ end
50
+ end
51
+ end
data/tests/inspec.yml ADDED
@@ -0,0 +1,7 @@
1
+ name: openpower_security
2
+ title: OpenPower Security Test Suite
3
+ maintainer: OSU Open Source Lab
4
+ copyright: Oregon State University
5
+ license: Apache License, Version 2.0
6
+ summary: Verify that an image has correctly configured security settings.
7
+ version: 1.0.0
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: openstack_taster
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - OSU Open Source Lab
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-07-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: inspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.10.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.10'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.10.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: fog-openstack
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.1.19
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.19
47
+ - !ruby/object:Gem::Dependency
48
+ name: net-ssh
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.2'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.2.0
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '3.2'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 3.2.0
67
+ - !ruby/object:Gem::Dependency
68
+ name: json
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.8'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.8.6
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '1.8'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 1.8.6
87
+ description: Tastes images on an OpenStack deployment for security and basic usability.
88
+ email: support@osuosl.org
89
+ executables:
90
+ - openstack_taster
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - bin/openstack_taster
95
+ - lib/openstack_taster.rb
96
+ - tests/controls/security_test.rb
97
+ - tests/inspec.yml
98
+ homepage: https://github.com/osuosl/openstack_taster
99
+ licenses:
100
+ - Apache-2.0
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 2.6.10
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Taste all of the OpenStack's basic functionality for an image
122
+ test_files: []