inspec 0.35.0 → 1.0.0.beta2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -2
  3. data/Gemfile +6 -0
  4. data/Rakefile +3 -55
  5. data/docs/README.md +20 -0
  6. data/docs/cli.rst +6 -0
  7. data/docs/dsl_inspec.md +245 -0
  8. data/docs/dsl_resource.md +93 -0
  9. data/docs/inspec_and_friends.md +102 -0
  10. data/docs/matchers.md +136 -0
  11. data/docs/plugin_kitchen_inspec.html.md +55 -0
  12. data/docs/profiles.md +271 -0
  13. data/docs/resources.rst +1 -1
  14. data/docs/shell.md +150 -0
  15. data/inspec.gemspec +1 -1
  16. data/lib/bundles/inspec-compliance/api.rb +28 -18
  17. data/lib/bundles/inspec-compliance/cli.rb +19 -27
  18. data/lib/fetchers/git.rb +4 -0
  19. data/lib/fetchers/local.rb +16 -1
  20. data/lib/fetchers/mock.rb +4 -0
  21. data/lib/fetchers/url.rb +40 -12
  22. data/lib/inspec/base_cli.rb +4 -0
  23. data/lib/inspec/cli.rb +6 -8
  24. data/lib/inspec/control_eval_context.rb +8 -0
  25. data/lib/inspec/dependencies/{vendor_index.rb → cache.rb} +5 -4
  26. data/lib/inspec/dependencies/dependency_set.rb +8 -14
  27. data/lib/inspec/dependencies/requirement.rb +10 -20
  28. data/lib/inspec/dependencies/resolver.rb +2 -2
  29. data/lib/inspec/dsl.rb +9 -0
  30. data/lib/inspec/fetcher.rb +1 -1
  31. data/lib/inspec/objects/test.rb +8 -2
  32. data/lib/inspec/plugins/fetcher.rb +11 -12
  33. data/lib/inspec/plugins/resource.rb +3 -0
  34. data/lib/inspec/profile.rb +60 -14
  35. data/lib/inspec/profile_context.rb +28 -7
  36. data/lib/inspec/resource.rb +17 -2
  37. data/lib/inspec/rspec_json_formatter.rb +80 -35
  38. data/lib/inspec/runner.rb +42 -18
  39. data/lib/inspec/shell.rb +5 -16
  40. data/lib/inspec/version.rb +1 -1
  41. data/lib/resources/apache_conf.rb +1 -1
  42. data/lib/resources/gem.rb +1 -0
  43. data/lib/resources/oneget.rb +1 -0
  44. data/lib/resources/os.rb +1 -1
  45. data/lib/resources/package.rb +3 -1
  46. data/lib/resources/pip.rb +1 -1
  47. data/lib/resources/ssl.rb +9 -11
  48. metadata +15 -15
  49. data/docs/dsl_inspec.rst +0 -259
  50. data/docs/dsl_resource.rst +0 -90
  51. data/docs/inspec_and_friends.rst +0 -85
  52. data/docs/matchers.rst +0 -137
  53. data/docs/profiles.rst +0 -169
  54. data/docs/readme.rst +0 -105
  55. data/docs/shell.rst +0 -130
  56. data/docs/template.rst +0 -51
data/docs/resources.rst CHANGED
@@ -2195,7 +2195,7 @@ This file can be queried via:
2195
2195
 
2196
2196
  .. code-block:: ruby
2197
2197
 
2198
- describe json('/paht/to/name.json') do
2198
+ describe json('/path/to/name.json') do
2199
2199
  its('name') { should eq 'hello' }
2200
2200
  its(['meta','creator']) { should eq 'John Doe' }
2201
2201
  its(['array', 1]) { should eq 'one' }
data/docs/shell.md ADDED
@@ -0,0 +1,150 @@
1
+ ---
2
+ title: InSpec Shell
3
+ ---
4
+
5
+ # InSpec Shell
6
+
7
+ The InSpec interactive shell is a pry based REPL that can be used to
8
+ quickly run InSpec controls and tests without having to write it to a
9
+ file. Its functionality is similar to `chef shell` - it provides a way
10
+ to exercise the InSpec DSL, its resources, tests and plugins without
11
+ having to create a profile or write a test file. See
12
+ [http://pryrepl.org/](http://pryrepl.org/) for an introduction to what pry is and what it can
13
+ do.
14
+
15
+ ## Launching the shell
16
+
17
+ If you are using InSpec from a platform-specific package (rpm, msi,
18
+ etc.) or from a chef prepared shell in ChefDK, you can directly launch
19
+ InSpec shell against your local machine using the following. See
20
+ <https://docs.chef.io/install_dk.html#set-system-ruby> for details.
21
+
22
+ ```bash
23
+ $ inspec shell
24
+ $ inspec help shell # This will describe inspec shell usage
25
+ ```
26
+
27
+ If you wish to connect to a remote machine (called a target within
28
+ InSpec), you can use the `-t` flag. We support connecting using ssh,
29
+ WinRm and docker. If no target is provided, we implicitly support the
30
+ "local" target - i.e. tests running on the current machine running
31
+ InSpec. For an ssh connection, use `-i` for specifying ssh key files,
32
+ and the `--sudo*` commands for requesting a privelege escalation after
33
+ logging in. For a WinRM connection, use `--path` to change the login
34
+ path, `--ssl` to use SSL for transport layer encryption.
35
+
36
+ ```bash
37
+ $ inspec shell -t ssh://root@192.168.64.2:11022 # Login to remote machine using ssh as root.
38
+ $ inspec shell -t ssh://user@hostname:1234 -i /path/to/user_key # Login to hostname on port 1234 as user using given ssh key.
39
+ $ inspec shell -t winrm://UserName:Password@windowsmachine:1234 # Login to windowsmachine over WinRM as UserName.
40
+ $ inspec shell -t docker://container_id # Login to a docker container.
41
+ ```
42
+
43
+ ## Using Ruby in InSpec shell
44
+
45
+ Since InSpec shell is pry based, you may treat the shell as an
46
+ interactive Ruby session. You may write Ruby expressions and evaluate
47
+ them. Source high-lighting, automatic indentation and command history
48
+ (using the up and down arrow keys) are available to make your experience
49
+ more delightful. You can exit the shell using `exit`.
50
+
51
+ ```bash
52
+ $ inspec shell
53
+ Welcome to the interactive InSpec Shell
54
+ To find out how to use it, type: help
55
+
56
+ inspec> 1 + 2
57
+ => 3
58
+ inspec> exit
59
+ ```
60
+
61
+ ## Using InSpec DSL in InSpec shell
62
+
63
+ InSpec shell will automatically evaluate the result of every command as
64
+ if it were a test file. If you type in a Ruby command that is not an
65
+ InSpec control or test, the shell will evaluate it as if it were a
66
+ regular ruby command.
67
+
68
+ Bare InSpec resources are instantiated and their help text is presented.
69
+ You may also access the resource contents or other matchers that they
70
+ define. Run `help <resource>` to get more help on using a particular
71
+ resource or see the InSpec resources documentation online.
72
+
73
+ ```bash
74
+ $ inspec shell
75
+ Welcome to the interactive InSpec Shell
76
+ To find out how to use it, type: help
77
+
78
+ inspec> file('/Users/ksubramanian').directory?
79
+ => true
80
+ inspec> os_env('HOME')
81
+ => Environment variable HOME
82
+ inspec> os_env('HOME').content
83
+ => /Users/ksubramanian
84
+ inspec> exit
85
+ ```
86
+
87
+ InSpec tests are immediately executed.
88
+
89
+ ```bash
90
+ inspec> describe file('/Users') # Empty test.
91
+ Summary: 0 successful, 0 failures, 0 skipped
92
+ inspec> describe file('/Users') do # Test with one check.
93
+ inspec> it { should exist }
94
+ inspec> end
95
+ ✔ File /Users should exist
96
+
97
+ Summary: 1 successful, 0 failures, 0 skipped
98
+ ```
99
+
100
+ All tests in a control are immediately executed as well. If a control is
101
+ redefined in the shell, the old control's tests are destroyed and
102
+ replaced with the redefinition and the control is re-run.
103
+
104
+ ```bash
105
+ inspec> control 'my_control' do
106
+ inspec> describe os_env('HOME') do
107
+ inspec> its('content') { should eq '/Users/ksubramanian' }
108
+ inspec> end
109
+ inspec> end
110
+ ✔ my_control: Environment variable HOME content should eq "/Users/ksubramanian"
111
+
112
+ Summary: 1 successful, 0 failures, 0 skipped
113
+ ```
114
+
115
+ Syntax errors are illegal tests are also detected and reported.
116
+
117
+ ```bash
118
+ inspec> control 'foo' do
119
+ inspec> thisisnonsense
120
+ inspec> end
121
+ NameError: undefined local variable or method `thisisnonsense' for #<#<Class:0x007fd63b571f98>:0x007fd639825cc8>
122
+ from /usr/local/lib/ruby/gems/2.3.0/gems/rspec-expectations-3.5.0/lib/rspec/matchers.rb:967:in `method_missing'
123
+ inspec> control 'foo' do
124
+ inspec> describe file('wut') do
125
+ inspec> its('thismakesnosense') { should cmp 'fail' }
126
+ inspec> end
127
+ inspec> end
128
+ ✖ foo: File wut thismakesnosense (undefined method `thismakesnosense' for File wut:Inspec::Resource::Registry::File)
129
+
130
+ Summary: 0 successful, 1 failures, 0 skipped
131
+ ```
132
+
133
+ ## Running a single InSpec command
134
+
135
+ If you wish to run a single InSpec command and fetch its results, you
136
+ may use the `-c` flag. This is similar to using `bash -c`.
137
+
138
+ ```bash
139
+ $ inspec shell -c 'describe file("/Users/ksubramanian") do it { should exist } end'}
140
+ Target: local://
141
+
142
+ ✔ File /Users/ksubramanian should exist
143
+
144
+ Summary: 1 successful, 0 failures, 0 skipped
145
+ ```
146
+
147
+ ```bash
148
+ $ inspec shell --format json -c 'describe file("/Users/ksubramanian") do it { should exist } end'
149
+ {"version":"0.30.0","profiles":{"":{"supports":[],"controls":{"(generated from in_memory.rb:1 5aab65c33fb1f133d9244017958eef64)":{"title":null,"desc":null,"impact":0.5,"refs":[],"tags":{},"code":" rule = rule_class.new(id, profile_id, {}) do\n res = describe(*args, &block)\n end\n","source_location":{"ref":"/Users/ksubramanian/repo/chef/inspec/lib/inspec/profile_context.rb","line":184},"results":[{"status":"passed","code_desc":"File /Users/ksubramanian should exist","run_time":0.000747,"start_time":"2016-08-16 11:41:40 -0400"}]}},"groups":{"in_memory.rb":{"title":null,"controls":["(generated from in_memory.rb:1 5aab65c33fb1f133d9244017958eef64)"]}},"attributes":[]}},"other_checks":[],"summary":{"duration":0.001078,"example_count":1,"failure_count":0,"skip_count":0}}}
150
+ ```
data/inspec.gemspec CHANGED
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
25
  spec.require_paths = ['lib']
26
26
 
27
- spec.add_dependency 'train', '>=0.19.0', '<1.0'
27
+ spec.add_dependency 'train', '>=0.19.1', '<1.0'
28
28
  spec.add_dependency 'thor', '~> 0.19'
29
29
  spec.add_dependency 'json', '>= 1.8', '< 3.0'
30
30
  spec.add_dependency 'rainbow', '~> 2'
@@ -9,18 +9,6 @@ module Compliance
9
9
  # API Implementation does not hold any state by itself,
10
10
  # everything will be stored in local Configuration store
11
11
  class API
12
- # login method for pre-1.0 compliance server
13
- def self.legacy_login_post(url, username, password, insecure)
14
- # form request
15
- # TODO: reuse post function
16
- uri = URI.parse(url)
17
- req = Net::HTTP::Post.new(uri.path)
18
- req.basic_auth(username, password)
19
- req.form_data={}
20
-
21
- send_request(uri, req, insecure)
22
- end
23
-
24
12
  # return all compliance profiles available for the user
25
13
  def self.profiles(config)
26
14
  url = "#{config['server']}/user/compliance"
@@ -86,19 +74,19 @@ Please login using `inspec compliance login https://compliance.test --user admin
86
74
  [res.is_a?(Net::HTTPSuccess), res.body]
87
75
  end
88
76
 
89
- def self.post_refresh_token(url, token, insecure)
77
+ # Use username and refresh_toke to get an API access token
78
+ def self.get_token_via_refresh_token(url, refresh_token, insecure)
90
79
  uri = URI.parse("#{url}/login")
91
80
  req = Net::HTTP::Post.new(uri.path)
92
- # req['Authorization'] = "Bearer #{token}"
93
- req.body = { token: token }.to_json
81
+ req.body = { token: refresh_token }.to_json
94
82
  access_token = nil
95
83
  response = Compliance::HTTP.send_request(uri, req, insecure)
96
84
  data = response.body
97
- if !data.nil?
85
+ if response.code == '200'
98
86
  begin
99
87
  tokendata = JSON.parse(data)
100
88
  access_token = tokendata['access_token']
101
- msg = 'Successfully fetched access token'
89
+ msg = 'Successfully fetched API access token'
102
90
  success = true
103
91
  rescue JSON::ParserError => e
104
92
  success = false
@@ -106,7 +94,29 @@ Please login using `inspec compliance login https://compliance.test --user admin
106
94
  end
107
95
  else
108
96
  success = false
109
- msg = 'Invalid refresh_token'
97
+ msg = "Failed to authenticate to #{url} \n\
98
+ Response code: #{response.code}\n Body: #{response.body}"
99
+ end
100
+
101
+ [success, msg, access_token]
102
+ end
103
+
104
+ # Use username and password to get an API access token
105
+ def self.get_token_via_password(url, username, password, insecure)
106
+ uri = URI.parse("#{url}/login")
107
+ req = Net::HTTP::Post.new(uri.path)
108
+ req.body = { userid: username, password: password }.to_json
109
+ access_token = nil
110
+ response = Compliance::HTTP.send_request(uri, req, insecure)
111
+ data = response.body
112
+ if response.code == '200'
113
+ access_token = data
114
+ msg = 'Successfully fetched an API access token valid for 12 hours'
115
+ success = true
116
+ else
117
+ success = false
118
+ msg = "Failed to authenticate to #{url} \n\
119
+ Response code: #{response.code}\n Body: #{response.body}"
110
120
  end
111
121
 
112
122
  [success, msg, access_token]
@@ -22,9 +22,9 @@ module Compliance
22
22
  option :insecure, aliases: :k, type: :boolean,
23
23
  desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
24
24
  option :user, type: :string, required: false,
25
- desc: 'Chef Compliance Username (for legacy auth)'
25
+ desc: 'Chef Compliance Username'
26
26
  option :password, type: :string, required: false,
27
- desc: 'Chef Compliance Password (for legacy auth)'
27
+ desc: 'Chef Compliance Password'
28
28
  option :apipath, type: :string, default: '/api',
29
29
  desc: 'Set the path to the API, defaults to /api'
30
30
  option :token, type: :string, required: false,
@@ -38,7 +38,7 @@ module Compliance
38
38
  url = options['server'] + options['apipath']
39
39
  if !options['user'].nil? && !options['password'].nil?
40
40
  # username / password
41
- _success, msg = login_legacy(url, options['user'], options['password'], options['insecure'])
41
+ _success, msg = login_username_password(url, options['user'], options['password'], options['insecure'])
42
42
  elsif !options['user'].nil? && !options['token'].nil?
43
43
  # access token
44
44
  _success, msg = store_access_token(url, options['user'], options['token'], options['insecure'])
@@ -199,7 +199,7 @@ module Compliance
199
199
  private
200
200
 
201
201
  def login_refreshtoken(url, options)
202
- success, msg, access_token = Compliance::API.post_refresh_token(url, options['refresh_token'], options['insecure'])
202
+ success, msg, access_token = Compliance::API.get_token_via_refresh_token(url, options['refresh_token'], options['insecure'])
203
203
  if success
204
204
  config = Compliance::Configuration.new
205
205
  config['server'] = url
@@ -212,25 +212,17 @@ module Compliance
212
212
  [success, msg]
213
213
  end
214
214
 
215
- def login_legacy(url, username, password, insecure)
215
+ def login_username_password(url, username, password, insecure)
216
216
  config = Compliance::Configuration.new
217
- success, data = Compliance::API.legacy_login_post(url+'/oauth/token', username, password, insecure)
218
- if !data.nil?
219
- tokendata = JSON.parse(data)
220
- if tokendata['access_token']
221
- config['server'] = url
222
- config['user'] = username
223
- config['token'] = tokendata['access_token']
224
- config['insecure'] = insecure
225
- config['version'] = Compliance::API.version(url, insecure)
226
- config.store
227
- success = true
228
- msg = 'Successfully authenticated'
229
- else
230
- msg = 'Response does not include a token'
231
- end
232
- else
233
- msg = "Authentication failed for Server: #{url}"
217
+ success, msg, api_token = Compliance::API.get_token_via_password(url, username, password, insecure)
218
+ if success
219
+ config['server'] = url
220
+ config['user'] = username
221
+ config['token'] = api_token
222
+ config['insecure'] = insecure
223
+ config['version'] = Compliance::API.version(url, insecure)
224
+ config.store
225
+ success = true
234
226
  end
235
227
  [success, msg]
236
228
  end
@@ -245,10 +237,10 @@ module Compliance
245
237
  config['version'] = Compliance::API.version(url, insecure)
246
238
  config.store
247
239
 
248
- [true, 'access token stored']
240
+ [true, 'API access token stored']
249
241
  end
250
242
 
251
- # saves the a user refresh token supplied by the user
243
+ # saves a refresh token supplied by the user
252
244
  def store_refresh_token(url, refresh_token, verify, user, insecure)
253
245
  config = Compliance::Configuration.new
254
246
  config['server'] = url
@@ -260,13 +252,13 @@ module Compliance
260
252
  if !verify
261
253
  config.store
262
254
  success = true
263
- msg = 'refresh token stored'
255
+ msg = 'API refresh token stored'
264
256
  else
265
- success, msg, access_token = Compliance::API.post_refresh_token(url, refresh_token, insecure)
257
+ success, msg, access_token = Compliance::API.get_token_via_refresh_token(url, refresh_token, insecure)
266
258
  if success
267
259
  config['token'] = access_token
268
260
  config.store
269
- msg = 'token verified and stored'
261
+ msg = 'API access token verified and stored'
270
262
  end
271
263
  end
272
264
 
data/lib/fetchers/git.rb CHANGED
@@ -56,6 +56,10 @@ module Fetchers
56
56
  @repo_directory
57
57
  end
58
58
 
59
+ def cache_key
60
+ resolved_ref
61
+ end
62
+
59
63
  def archive_path
60
64
  @repo_directory
61
65
  end
@@ -55,8 +55,23 @@ module Fetchers
55
55
  @target
56
56
  end
57
57
 
58
+ def writable?
59
+ File.directory?(@target)
60
+ end
61
+
62
+ def cache_key
63
+ sha256.to_s
64
+ end
65
+
66
+ def sha256
67
+ return nil if File.directory?(@target)
68
+ @archive_shasum ||= Digest::SHA256.hexdigest File.read(@target)
69
+ end
70
+
58
71
  def resolved_source
59
- { path: @target }
72
+ h = { path: @target }
73
+ h[:sha256] = sha256 if sha256
74
+ h
60
75
  end
61
76
  end
62
77
  end
data/lib/fetchers/mock.rb CHANGED
@@ -27,5 +27,9 @@ module Fetchers
27
27
  def resolved_source
28
28
  { mock_fetcher: true }
29
29
  end
30
+
31
+ def cache_key
32
+ ''
33
+ end
30
34
  end
31
35
  end
data/lib/fetchers/url.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  # author: Christoph Hartmann
4
4
 
5
5
  require 'uri'
6
+ require 'digest'
6
7
  require 'tempfile'
7
8
  require 'open-uri'
8
9
 
@@ -81,34 +82,61 @@ module Fetchers
81
82
  end
82
83
 
83
84
  def fetch(path)
84
- Inspec::Log.debug("Fetching URL: #{@target}")
85
- @archive_path = download_archive(path)
85
+ @archive_path ||= download_archive(path)
86
+ end
87
+
88
+ def sha256
89
+ c = if @archive_path
90
+ File.read(@archive_path)
91
+ else
92
+ content
93
+ end
94
+ Digest::SHA256.hexdigest c
86
95
  end
87
96
 
88
97
  def resolved_source
89
- { url: @target }
98
+ @resolved_source ||= { url: @target, sha256: sha256 }
99
+ end
100
+
101
+ def cache_key
102
+ sha256
103
+ end
104
+
105
+ def to_s
106
+ @target
90
107
  end
91
108
 
92
109
  private
93
110
 
94
- # download url into archive using opts,
95
- # returns File object and content-type from HTTP headers
96
- def download_archive(path)
111
+ def open_target
112
+ Inspec::Log.debug("Fetching URL: #{@target}")
97
113
  http_opts = {}
98
114
  http_opts['ssl_verify_mode'.to_sym] = OpenSSL::SSL::VERIFY_NONE if @insecure
99
115
  http_opts['Authorization'] = "Bearer #{@token}" if @token
116
+ open(@target, http_opts)
117
+ end
100
118
 
101
- remote = open(@target, http_opts)
119
+ def content
120
+ open_target.read
121
+ end
102
122
 
123
+ def file_type_from_remote(remote)
103
124
  content_type = remote.meta['content-type']
104
- file_type = MIME_TYPES[content_type] ||
105
- throw(RuntimeError, 'Failed to resolve URL target, its '\
106
- "metadata did not match ZIP or TAR: #{content_type}")
125
+ file_type = MIME_TYPES[content_type]
107
126
 
108
- # fall back to tar
109
127
  if file_type.nil?
110
- fail "Could not determine file type for content type #{content_type}."
128
+ Inspec::Log.warn("Unrecognized content type: #{content_type}. Assuming tar.gz")
129
+ file_type = '.tar.gz'
111
130
  end
131
+
132
+ file_type
133
+ end
134
+
135
+ # download url into archive using opts,
136
+ # returns File object and content-type from HTTP headers
137
+ def download_archive(path)
138
+ remote = open_target
139
+ file_type = file_type_from_remote(remote)
112
140
  final_path = "#{path}#{file_type}"
113
141
  # download content
114
142
  archive = Tempfile.new(['inspec-dl-', file_type])