inspec 0.35.0 → 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
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])