kitchen-inspec 0.25.0 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "kitchen/verifier/inspec_version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kitchen-inspec"
8
+ spec.version = Kitchen::Verifier::INSPEC_VERSION
9
+ spec.license = "Apache-2.0"
10
+ spec.authors = ["Chef Software, Inc."]
11
+ spec.email = ["info@chef.io"]
12
+
13
+ spec.summary = "A Test Kitchen Verifier for InSpec"
14
+ spec.description = spec.summary
15
+ spec.homepage = "https://github.com/inspec/kitchen-inspec"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ .grep(/LICENSE|^lib|/)
19
+ spec.require_paths = ["lib"]
20
+ spec.required_ruby_version = ">= 2.1.0"
21
+ spec.add_dependency "inspec", ">=0.34.0", "<4.0.0"
22
+ spec.add_dependency "test-kitchen", "~> 1.6"
23
+ spec.add_dependency "hashie", "~> 3.4"
24
+ end
@@ -20,6 +20,6 @@
20
20
  module Kitchen
21
21
  module Verifier
22
22
  # Version string for InSpec Kitchen verifier
23
- INSPEC_VERSION = "0.25.0"
23
+ INSPEC_VERSION = "0.26.0"
24
24
  end
25
25
  end
@@ -0,0 +1,607 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Author:: Fletcher Nichol (<fnichol@chef.io>)
4
+ # Author:: Christoph Hartmann (<chartmann@chef.io>)
5
+ #
6
+ # Copyright (C) 2015, Chef Software Inc.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require_relative "../../spec_helper"
21
+
22
+ require "logger"
23
+
24
+ require "kitchen/verifier/inspec"
25
+ require "kitchen/transport/exec"
26
+ require "kitchen/transport/ssh"
27
+ require "kitchen/transport/winrm"
28
+
29
+ describe Kitchen::Verifier::Inspec do
30
+
31
+ let(:logged_output) { StringIO.new }
32
+ let(:logger) { Logger.new(logged_output) }
33
+ let(:config) do
34
+ {
35
+ kitchen_root: kitchen_root,
36
+ test_base_path: File.join(kitchen_root, "test", "integration"),
37
+ backend_cache: true,
38
+ reporter: [
39
+ "cli",
40
+ "junit:path/to/results/%{platform}_%{suite}_inspec.xml",
41
+ ],
42
+ }
43
+ end
44
+ let(:transport_config) { {} }
45
+ let(:kitchen_root) { Dir.mktmpdir }
46
+
47
+ let(:platform) do
48
+ instance_double("Kitchen::Platform", os_type: nil, shell_type: nil, name: "default")
49
+ end
50
+
51
+ let(:suite) do
52
+ instance_double("Kitchen::Suite", name: "germany")
53
+ end
54
+
55
+ let(:transport) do
56
+ instance_double(
57
+ "Kitchen::Transport::Dummy",
58
+ name: "wickedsauce",
59
+ diagnose: transport_config
60
+ )
61
+ end
62
+
63
+ let(:instance) do
64
+ instance_double(
65
+ "Kitchen::Instance",
66
+ name: "coolbeans",
67
+ logger: logger,
68
+ platform: platform,
69
+ suite: suite,
70
+ transport: transport,
71
+ to_str: "instance"
72
+ )
73
+ end
74
+
75
+ before do
76
+ allow(transport).to receive(:instance).and_return(instance)
77
+ end
78
+
79
+ after do
80
+ FileUtils.remove_entry(kitchen_root)
81
+ end
82
+
83
+ let(:verifier) do
84
+ Kitchen::Verifier::Inspec.new(config).finalize_config!(instance)
85
+ end
86
+
87
+ it "verifier api_version is 1" do
88
+ expect(verifier.diagnose_plugin[:api_version]).to eq(1)
89
+ end
90
+
91
+ it "plugin_version is set to Kitchen::Verifier::INSPEC_VERSION" do
92
+ expect(verifier.diagnose_plugin[:version])
93
+ .to eq(Kitchen::Verifier::INSPEC_VERSION)
94
+ end
95
+
96
+ describe "configuration" do
97
+ let(:transport) do
98
+ Kitchen::Transport::Ssh.new({})
99
+ end
100
+
101
+ it "supports reporter config platform and suite replacements" do
102
+ config = verifier.send(:runner_options, transport, {}, "osx", "internal")
103
+ expected_value = [
104
+ "cli",
105
+ "junit:path/to/results/osx_internal_inspec.xml",
106
+ ]
107
+
108
+ expect(config.to_hash).to include("reporter" => expected_value)
109
+ end
110
+
111
+ it "backend_cache option sets to true" do
112
+ config = verifier.send(:runner_options, transport)
113
+ expect(config.to_hash).to include(backend_cache: true)
114
+ end
115
+
116
+ it "backend_cache option defaults to false" do
117
+ config[:backend_cache] = nil
118
+ config = verifier.send(:runner_options, transport)
119
+ expect(config.to_hash).to include(backend_cache: false)
120
+ end
121
+
122
+ it "inspec version warn for backend_cache" do
123
+ config[:backend_cache] = true
124
+ stub_const("Inspec::VERSION", "1.46.0")
125
+ expect_any_instance_of(Logger).to receive(:warn).
126
+ with("backend_cache requires InSpec version >= 1.47.0").
127
+ and_return("captured")
128
+ config = verifier.send(:runner_options, transport)
129
+ expect(config.to_hash).to include(backend_cache: true)
130
+ end
131
+ end
132
+
133
+ describe "#finalize_config!" do
134
+ let(:kitchen_inspec_tests) { File.join(kitchen_root, "test", "recipes") }
135
+ context "when a test/recipes folder exists" do
136
+ before do
137
+ FileUtils.mkdir_p(kitchen_inspec_tests)
138
+ end
139
+
140
+ it "should read the tests from there" do
141
+ expect(verifier[:test_base_path]).to eq(kitchen_inspec_tests)
142
+ end
143
+ end
144
+
145
+ context "when a test/recipes folder does not exist" do
146
+ it "should read the tests from the default location" do
147
+ expect(verifier[:test_base_path]).to eq(File.join(kitchen_root, "test", "integration"))
148
+ end
149
+ end
150
+ end
151
+
152
+ describe "#resolve_config_inspec_tests" do
153
+ context "when the entry is a string" do
154
+ context "when the path does not exist" do
155
+ it "returns the original string" do
156
+ config[:inspec_tests] = ["test/integration/foo"]
157
+ expect(File).to receive(:exist?).with("test/integration/foo").and_return(false)
158
+ allow(File).to receive(:exist?).and_call_original
159
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq(["test/integration/foo"])
160
+ end
161
+ end
162
+
163
+ context "when the path exists" do
164
+ it "expands to an absolute path and returns a hash" do
165
+ config[:inspec_tests] = ["test/integration/foo"]
166
+ expect(File).to receive(:exist?).with("test/integration/foo").and_return(true)
167
+ allow(File).to receive(:exist?).and_call_original
168
+ expect(File).to receive(:expand_path).with("test/integration/foo").and_return("/absolute/path/to/foo")
169
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([{ path: "/absolute/path/to/foo" }])
170
+ end
171
+ end
172
+ end
173
+
174
+ context "when the entry is a hash" do
175
+ context "when the entry is a path" do
176
+ it "expands the path to an absolute path and removes unnecessary keys" do
177
+ config[:inspec_tests] = [{ name: "foo_profile", path: "test/integration/foo" }]
178
+ expect(File).to receive(:expand_path).with("test/integration/foo").and_return("/absolute/path/to/foo")
179
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([{ path: "/absolute/path/to/foo" }])
180
+ end
181
+ end
182
+
183
+ context "when the entry is a url item" do
184
+ it "returns a hash with unnecessary keys removed" do
185
+ config[:inspec_tests] = [{ name: "foo_profile", url: "http://some.domain/profile" }]
186
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([{ url: "http://some.domain/profile" }])
187
+ end
188
+ end
189
+
190
+ context "when the entry is a git item" do
191
+ it "returns a hash with unnecessary keys removed" do
192
+ config[:inspec_tests] = [{ name: "foo_profile", git: "http://some.domain/profile" }]
193
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([{ git: "http://some.domain/profile" }])
194
+ end
195
+ end
196
+
197
+ context "when the entry is a compliance item" do
198
+ it "returns a hash with unnecessary keys removed" do
199
+ config[:inspec_tests] = [{ name: "foo_profile", compliance: "me/foo" }]
200
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([{ compliance: "me/foo" }])
201
+ end
202
+ end
203
+
204
+ context "when the entry only contains a name" do
205
+ it "returns it as-is to be resolved on Supermarket" do
206
+ config[:inspec_tests] = [{ name: "me/foo" }]
207
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([{ name: "me/foo" }])
208
+ end
209
+ end
210
+
211
+ context "when the entry contains no acceptable keys" do
212
+ it "returns nil" do
213
+ config[:inspec_tests] = [{ key1: "value1", key2: "value2" }]
214
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([nil])
215
+ end
216
+ end
217
+ end
218
+
219
+ it "returns an array of properly formatted entries when multiple entries are supplied" do
220
+ config[:inspec_tests] = [
221
+ { name: "profile1", git: "me/profile1" },
222
+ { name: "profile2", random_key: "random_value", compliance: "me/profile2" },
223
+ { name: "profile3", url: "someurl", random_key: "what is this for?", another_random_key: 123 },
224
+ { name: "profile4" },
225
+ ]
226
+
227
+ expect(verifier.send(:resolve_config_inspec_tests)).to eq([
228
+ { git: "me/profile1" },
229
+ { compliance: "me/profile2" },
230
+ { url: "someurl" },
231
+ { name: "profile4" },
232
+ ])
233
+ end
234
+ end
235
+
236
+ context "with an ssh transport" do
237
+
238
+ let(:transport_config) do
239
+ {
240
+ hostname: "boogie",
241
+ port: "I shouldn't be used",
242
+ username: "dance",
243
+ ssh_key: "/backstage/pass",
244
+ keepalive: "keepalive",
245
+ keepalive_interval: "forever",
246
+ connection_timeout: "nope",
247
+ connection_retries: "thousand",
248
+ connection_retry_sleep: "sleepy",
249
+ max_wait_until_ready: 42,
250
+ compression: "maxyo",
251
+ compression_level: "pico",
252
+ }
253
+ end
254
+
255
+ let(:transport) do
256
+ Kitchen::Transport::Ssh.new(transport_config)
257
+ end
258
+
259
+ let(:runner) do
260
+ instance_double("Inspec::Runner")
261
+ end
262
+
263
+ before do
264
+ allow(runner).to receive(:add_target)
265
+ allow(runner).to receive(:run).and_return 0
266
+ end
267
+
268
+ it "constructs a Inspec::Runner using transport config data and state" do
269
+ config[:sudo] = "jellybeans"
270
+ config[:sudo_command] = "allyourbase"
271
+ config[:proxy_command] = "gateway"
272
+
273
+ expect(Inspec::Runner).to receive(:new)
274
+ .with(
275
+ hash_including(
276
+ "backend" => "ssh",
277
+ "logger" => logger,
278
+ "sudo" => "jellybeans",
279
+ "sudo_command" => "allyourbase",
280
+ "host" => "boogie",
281
+ "port" => 123,
282
+ "user" => "dance",
283
+ "keepalive" => "keepalive",
284
+ "keepalive_interval" => "forever",
285
+ "connection_timeout" => "nope",
286
+ "connection_retries" => "thousand",
287
+ "connection_retry_sleep" => "sleepy",
288
+ "max_wait_until_ready" => 42,
289
+ "compression" => "maxyo",
290
+ "compression_level" => "pico",
291
+ "key_files" => ["/backstage/pass"],
292
+ "proxy_command" => "gateway"
293
+ )
294
+ )
295
+ .and_return(runner)
296
+
297
+ verifier.call(port: 123)
298
+ end
299
+
300
+ it "constructs a Inspec::Runner using transport config data(host and port)" do
301
+ config[:host] = "192.168.33.40"
302
+ config[:port] = 222
303
+
304
+ expect(Inspec::Runner).to receive(:new)
305
+ .with(
306
+ hash_including(
307
+ "backend" => "ssh",
308
+ "host" => "192.168.33.40",
309
+ "port" => 222
310
+ )
311
+ )
312
+ .and_return(runner)
313
+
314
+ verifier.call(port: 123)
315
+ end
316
+
317
+ it "constructs an Inspec::Runner with a specified inspec output format" do
318
+ config[:format] = "documentation"
319
+
320
+ expect(Inspec::Runner).to receive(:new)
321
+ .with(
322
+ hash_including(
323
+ "format" => "documentation"
324
+ )
325
+ )
326
+ .and_return(runner)
327
+
328
+ verifier.call(port: 123)
329
+ end
330
+
331
+ it "constructs an Inspec::Runner with a controls filter" do
332
+ config[:controls] = %w{a control}
333
+
334
+ expect(Inspec::Runner).to receive(:new)
335
+ .with(
336
+ hash_including(
337
+ controls: %w{a control}
338
+ )
339
+ )
340
+ .and_return(runner)
341
+
342
+ verifier.call(port: 123)
343
+ end
344
+
345
+ it "does not send keys_only=true to InSpec (which breaks SSH Agent usage)" do
346
+ expect(Inspec::Runner).to receive(:new)
347
+ .with(
348
+ hash_not_including(
349
+ "keys_only" => true
350
+ )
351
+ )
352
+ .and_return(runner)
353
+
354
+ verifier.call(port: 123)
355
+ end
356
+
357
+ it "provide platform and test suite to build output path" do
358
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
359
+
360
+ expect(verifier).to receive(:runner_options).with(
361
+ transport,
362
+ {},
363
+ "default",
364
+ "germany"
365
+ ).and_return({})
366
+ verifier.call({})
367
+ end
368
+
369
+ it "custom inspec output path" do
370
+ ensure_suite_directory("germany")
371
+ config[:output] = "/tmp/inspec_results.xml"
372
+
373
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
374
+
375
+ expect(runner).to receive(:add_target).with({ :path =>
376
+ File.join(
377
+ config[:test_base_path],
378
+ "germany"
379
+ ) }, hash_including(
380
+ "output" => "/tmp/inspec_results.xml"
381
+ ))
382
+
383
+ verifier.call({})
384
+ end
385
+
386
+ it "resolve template format for inspec output path" do
387
+ ensure_suite_directory("germany")
388
+ config[:output] = "/tmp/%{platform}_%{suite}.xml"
389
+
390
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
391
+
392
+ expect(runner).to receive(:add_target).with({ :path =>
393
+ File.join(
394
+ config[:test_base_path],
395
+ "germany"
396
+ ) }, hash_including(
397
+ "output" => "/tmp/default_germany.xml"
398
+ ))
399
+
400
+ verifier.call({})
401
+ end
402
+
403
+ it "find test directory for runner" do
404
+ ensure_suite_directory("germany")
405
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
406
+ expect(runner).to receive(:add_target).with({ :path =>
407
+ File.join(
408
+ config[:test_base_path],
409
+ "germany"
410
+ ) }, anything)
411
+
412
+ verifier.call({})
413
+ end
414
+
415
+ it "find test directory for runner if legacy" do
416
+ create_legacy_test_directories
417
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
418
+ expect(runner).to receive(:add_target).with({ :path =>
419
+ File.join(
420
+ config[:test_base_path],
421
+ "germany", "inspec"
422
+ ) }, anything)
423
+
424
+ verifier.call({})
425
+ end
426
+
427
+ it "non-existent test directory for runner" do
428
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
429
+ expect(runner).to_not receive(:add_target).with(
430
+ File.join(
431
+ config[:test_base_path],
432
+ "nobody"
433
+ ), anything)
434
+
435
+ verifier.call({})
436
+ end
437
+
438
+ it "calls #run on the runner" do
439
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
440
+ expect(runner).to receive(:run)
441
+
442
+ verifier.call({})
443
+ end
444
+ end
445
+
446
+ context "with an remote profile" do
447
+
448
+ let(:transport) do
449
+ Kitchen::Transport::Ssh.new({})
450
+ end
451
+
452
+ let(:runner) do
453
+ instance_double("Inspec::Runner")
454
+ end
455
+
456
+ let(:suite) do
457
+ instance_double("Kitchen::Suite", { name: "local" })
458
+ end
459
+
460
+ let(:instance) do
461
+ instance_double(
462
+ "Kitchen::Instance",
463
+ name: "coolbeans",
464
+ logger: logger,
465
+ platform: platform,
466
+ suite: suite,
467
+ transport: transport,
468
+ to_str: "instance"
469
+ )
470
+ end
471
+
472
+ let(:config) do
473
+ {
474
+ inspec_tests: [{ :url => "https://github.com/nathenharvey/tmp_compliance_profile" }],
475
+ kitchen_root: kitchen_root,
476
+ test_base_path: File.join(kitchen_root, "test", "integration"),
477
+ }
478
+ end
479
+
480
+ before do
481
+ allow(runner).to receive(:add_target)
482
+ allow(runner).to receive(:run).and_return 0
483
+ end
484
+
485
+ it "find test directory and remote profile" do
486
+ ensure_suite_directory("local")
487
+ allow(Inspec::Runner).to receive(:new).and_return(runner)
488
+ expect(runner).to receive(:add_target).with({ :path =>
489
+ File.join(config[:test_base_path], "local") }, anything)
490
+ expect(runner).to receive(:add_target).with(
491
+ { :url => "https://github.com/nathenharvey/tmp_compliance_profile" }, anything)
492
+ verifier.call({})
493
+ end
494
+ end
495
+
496
+ context "with an winrm transport" do
497
+
498
+ let(:transport_config) do
499
+ {
500
+ username: "dance",
501
+ password: "party",
502
+ connection_retries: "thousand",
503
+ connection_retry_sleep: "sleepy",
504
+ max_wait_until_ready: 42,
505
+ }
506
+ end
507
+
508
+ let(:transport) do
509
+ Kitchen::Transport::Winrm.new(transport_config)
510
+ end
511
+
512
+ let(:runner) do
513
+ instance_double("Inspec::Runner")
514
+ end
515
+
516
+ before do
517
+ allow(runner).to receive(:add_target)
518
+ allow(runner).to receive(:run).and_return 0
519
+ end
520
+
521
+ it "constructs a Inspec::Runner using transport config data and state" do
522
+ expect(Inspec::Runner).to receive(:new)
523
+ .with(
524
+ hash_including(
525
+ "backend" => "winrm",
526
+ "logger" => logger,
527
+ "host" => "win.dows",
528
+ "port" => 123,
529
+ "user" => "dance",
530
+ "password" => "party",
531
+ "connection_retries" => "thousand",
532
+ "connection_retry_sleep" => "sleepy",
533
+ "max_wait_until_ready" => 42,
534
+ "color" => true
535
+ )
536
+ )
537
+ .and_return(runner)
538
+
539
+ verifier.call(hostname: "win.dows", port: 123)
540
+ end
541
+
542
+ it "constructs a Inspec::Runner using transport config data(host and port)" do
543
+ config[:host] = "192.168.56.40"
544
+ config[:port] = 22
545
+
546
+ expect(Inspec::Runner).to receive(:new)
547
+ .with(
548
+ hash_including(
549
+ "backend" => "winrm",
550
+ "host" => "192.168.56.40",
551
+ "port" => 22
552
+ )
553
+ )
554
+ .and_return(runner)
555
+
556
+ verifier.call(hostname: "win.dows", port: 123)
557
+ end
558
+ end
559
+
560
+ context "with an exec transport" do
561
+
562
+ let(:transport) do
563
+ Kitchen::Transport::Exec.new
564
+ end
565
+
566
+ let(:runner) do
567
+ instance_double("Inspec::Runner")
568
+ end
569
+
570
+ before do
571
+ allow(runner).to receive(:add_target)
572
+ allow(runner).to receive(:run).and_return 0
573
+ end
574
+
575
+ it "constructs a Inspec::Runner using transport config data and state" do
576
+ expect(Inspec::Runner).to receive(:new)
577
+ .with(
578
+ hash_including(
579
+ "backend" => "local",
580
+ "logger" => logger,
581
+ "color" => true
582
+ )
583
+ )
584
+ .and_return(runner)
585
+
586
+ verifier.call({})
587
+ end
588
+ end
589
+
590
+ context "with an unsupported transport" do
591
+
592
+ it "#call raises a UserError" do
593
+ expect { verifier.call({}) }.to raise_error(Kitchen::UserError)
594
+ end
595
+ end
596
+
597
+ def create_legacy_test_directories
598
+ base = File.join(config[:test_base_path], "germany")
599
+ FileUtils.mkdir_p(File.join(base, "inspec"))
600
+ FileUtils.mkdir_p(File.join(base, "serverspec"))
601
+ end
602
+
603
+ def ensure_suite_directory(suitename)
604
+ suite = File.join(config[:test_base_path], suitename)
605
+ FileUtils.mkdir_p(suite)
606
+ end
607
+ end