inspec 0.20.1 → 0.21.0

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -2
  3. data/docs/dsl_inspec.rst +2 -2
  4. data/docs/resources.rst +9 -9
  5. data/docs/ruby_usage.rst +145 -0
  6. data/inspec.gemspec +1 -0
  7. data/lib/bundles/inspec-compliance/cli.rb +15 -2
  8. data/lib/inspec/cli.rb +23 -10
  9. data/lib/inspec/dsl.rb +0 -52
  10. data/lib/inspec/objects/or_test.rb +1 -0
  11. data/lib/inspec/objects/test.rb +4 -4
  12. data/lib/inspec/profile.rb +76 -61
  13. data/lib/inspec/profile_context.rb +12 -11
  14. data/lib/inspec/rspec_json_formatter.rb +93 -40
  15. data/lib/inspec/rule.rb +7 -29
  16. data/lib/inspec/runner.rb +15 -4
  17. data/lib/inspec/runner_mock.rb +1 -1
  18. data/lib/inspec/runner_rspec.rb +26 -24
  19. data/lib/inspec/version.rb +1 -1
  20. data/lib/matchers/matchers.rb +3 -3
  21. data/lib/resources/auditd_rules.rb +2 -2
  22. data/lib/resources/host.rb +1 -1
  23. data/lib/resources/interface.rb +1 -1
  24. data/lib/resources/kernel_parameter.rb +1 -1
  25. data/lib/resources/mount.rb +2 -1
  26. data/lib/resources/mysql_session.rb +1 -1
  27. data/lib/resources/os_env.rb +2 -2
  28. data/lib/resources/passwd.rb +33 -93
  29. data/lib/resources/port.rb +47 -3
  30. data/lib/resources/processes.rb +3 -3
  31. data/lib/resources/service.rb +33 -1
  32. data/lib/resources/user.rb +15 -15
  33. data/lib/utils/base_cli.rb +1 -3
  34. data/lib/utils/filter.rb +30 -7
  35. data/test/cookbooks/os_prepare/recipes/_upstart_service_centos.rb +4 -0
  36. data/test/functional/helper.rb +1 -0
  37. data/test/functional/inheritance_test.rb +1 -1
  38. data/test/functional/inspec_compliance_test.rb +4 -3
  39. data/test/functional/inspec_exec_json_test.rb +122 -0
  40. data/test/functional/inspec_exec_test.rb +23 -117
  41. data/test/functional/{inspec_json_test.rb → inspec_json_profile_test.rb} +13 -15
  42. data/test/functional/inspec_test.rb +15 -2
  43. data/test/helper.rb +5 -1
  44. data/test/integration/default/auditd_rules_spec.rb +3 -3
  45. data/test/integration/default/kernel_parameter_spec.rb +6 -6
  46. data/test/integration/default/service_spec.rb +4 -0
  47. data/test/resource/command_test.rb +9 -9
  48. data/test/resource/dsl_test.rb +1 -1
  49. data/test/resource/file_test.rb +17 -17
  50. data/test/unit/control_test.rb +1 -1
  51. data/test/unit/mock/cmd/hpux-netstat-inet +10 -0
  52. data/test/unit/mock/cmd/hpux-netstat-inet6 +11 -0
  53. data/test/unit/mock/profiles/skippy-profile-os/controls/one.rb +1 -1
  54. data/test/unit/profile_context_test.rb +2 -2
  55. data/test/unit/profile_test.rb +11 -14
  56. data/test/unit/resources/passwd_test.rb +13 -14
  57. data/test/unit/resources/port_test.rb +14 -0
  58. data/test/unit/resources/processes_test.rb +3 -3
  59. data/test/unit/resources/service_test.rb +103 -39
  60. data/test/unit/utils/filter_table_test.rb +35 -3
  61. metadata +25 -4
@@ -3,7 +3,6 @@
3
3
  # author: Dominik Richter
4
4
 
5
5
  require 'utils/parser'
6
-
7
6
  # Usage:
8
7
  # describe port(80) do
9
8
  # it { should be_listening }
@@ -47,6 +46,8 @@ module Inspec::Resources
47
46
  @port_manager = FreeBsdPorts.new(inspec)
48
47
  elsif os.solaris?
49
48
  @port_manager = SolarisPorts.new(inspec)
49
+ elsif os.hpux?
50
+ @port_manager = HpuxPorts.new(inspec)
50
51
  else
51
52
  return skip_resource 'The `port` resource is not supported on your OS yet.'
52
53
  end
@@ -292,7 +293,6 @@ module Inspec::Resources
292
293
  # parse each line
293
294
  # 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - State, 7 - Inode, 8 - PID/Program name
294
295
  parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)\s+(\S+)\s+(\S+)/.match(line)
295
-
296
296
  return {} if parsed.nil? || line.match(/^proto/i)
297
297
 
298
298
  # parse ip4 and ip6 addresses
@@ -408,7 +408,7 @@ module Inspec::Resources
408
408
  # parse the content
409
409
  netstat_ports = parse_netstat(cmd.stdout)
410
410
 
411
- # filter all ports, where we listen
411
+ # filter all ports, where we `listen`
412
412
  listen = netstat_ports.select { |val|
413
413
  !val['state'].nil? && 'listen'.casecmp(val['state']) == 0
414
414
  }
@@ -433,4 +433,48 @@ module Inspec::Resources
433
433
  ports
434
434
  end
435
435
  end
436
+
437
+ # extracts information from netstat for hpux
438
+ class HpuxPorts < FreeBsdPorts
439
+ def info
440
+ ## Can't use 'netstat -an -f inet -f inet6' as the latter -f option overrides the former one and return only inet ports
441
+ cmd1 = inspec.command('netstat -an -f inet')
442
+ return nil if cmd1.exit_status.to_i != 0
443
+ cmd2 = inspec.command('netstat -an -f inet6')
444
+ return nil if cmd2.exit_status.to_i != 0
445
+ cmd = cmd1.stdout + cmd2.stdout
446
+ ports = []
447
+ # parse all lines
448
+ cmd.each_line do |line|
449
+ port_info = parse_netstat_line(line)
450
+ next unless %w{tcp tcp6 udp udp6}.include?(port_info[:protocol])
451
+ ports.push(port_info)
452
+ end
453
+ # select all ports, where we `listen`
454
+ ports.select { |val| val if 'listen'.casecmp(val[:state]) == 0 }
455
+ end
456
+
457
+ def parse_netstat_line(line)
458
+ # parse each line
459
+ # 1 - Proto, 2 - Recv-Q, 3 - Send-Q, 4 - Local Address, 5 - Foreign Address, 6 - (state)
460
+ parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?/.match(line)
461
+
462
+ return {} if parsed.nil? || line.match(/^proto/i) || line.match(/^active/i)
463
+ protocol = parsed[1].downcase
464
+ state = parsed[6].nil?? ' ' : parsed[6].downcase
465
+ local_addr = parsed[4]
466
+ local_addr[local_addr.rindex('.')] = ':'
467
+ # extract host and port information
468
+ host, port = parse_net_address(local_addr, protocol)
469
+ # map data
470
+ {
471
+ port: port,
472
+ address: host,
473
+ protocol: protocol,
474
+ state: state,
475
+ process: nil,
476
+ pid: nil,
477
+ }
478
+ end
479
+ end
436
480
  end
@@ -58,11 +58,11 @@ module Inspec::Resources
58
58
  lines.map do |m|
59
59
  {
60
60
  user: m[1],
61
- pid: m[2],
61
+ pid: m[2].to_i,
62
62
  cpu: m[3],
63
63
  mem: m[4],
64
- vsz: m[5],
65
- rss: m[6],
64
+ vsz: m[5].to_i,
65
+ rss: m[6].to_i,
66
66
  tty: m[7],
67
67
  stat: m[8],
68
68
  start: m[9],
@@ -3,6 +3,7 @@
3
3
  # author: Dominik Richter
4
4
  # author: Stephan Renatus
5
5
  # license: All rights reserved
6
+ require 'hashie'
6
7
 
7
8
  module Inspec::Resources
8
9
  class Runlevels < Hash
@@ -67,7 +68,7 @@ module Inspec::Resources
67
68
  # Ubuntu < 15.04 : upstart
68
69
  #
69
70
  # TODO: extend the logic to detect the running init system, independently of OS
70
- class Service < Inspec.resource(1)
71
+ class Service < Inspec.resource(1) # rubocop:disable ClassLength
71
72
  name 'service'
72
73
  desc 'Use the service InSpec audit resource to test if the named service is installed, running and/or enabled.'
73
74
  example "
@@ -75,11 +76,16 @@ module Inspec::Resources
75
76
  it { should be_installed }
76
77
  it { should be_enabled }
77
78
  it { should be_running }
79
+ its('type') { should be 'systemd' }
78
80
  end
79
81
 
80
82
  describe service('service_name').runlevels(3, 5) do
81
83
  it { should be_enabled }
82
84
  end
85
+
86
+ describe service('service_name').params do
87
+ its('UnitFileState') { should eq 'enabled' }
88
+ end
83
89
  "
84
90
 
85
91
  attr_reader :service_ctl
@@ -163,6 +169,11 @@ module Inspec::Resources
163
169
  info[:enabled]
164
170
  end
165
171
 
172
+ def params
173
+ return {} if info.nil?
174
+ Hashie::Mash.new(info[:params] || {})
175
+ end
176
+
166
177
  # verifies the service is registered
167
178
  def installed?(_name = nil, _version = nil)
168
179
  return false if info.nil?
@@ -181,9 +192,29 @@ module Inspec::Resources
181
192
  Runlevels.from_hash(self, info[:runlevels], args)
182
193
  end
183
194
 
195
+ # returns the service type from info
196
+ def type
197
+ return nil if info.nil?
198
+ info[:type]
199
+ end
200
+
201
+ # returns the service name from info
202
+ def name
203
+ return @service_name if info.nil?
204
+ info[:name]
205
+ end
206
+
207
+ # returns the service description from info
208
+ def description
209
+ return nil if info.nil?
210
+ info[:description]
211
+ end
212
+
184
213
  def to_s
185
214
  "Service #{@service_name}"
186
215
  end
216
+
217
+ private :info
187
218
  end
188
219
 
189
220
  class ServiceManager
@@ -229,6 +260,7 @@ module Inspec::Resources
229
260
  running: running,
230
261
  enabled: enabled,
231
262
  type: 'systemd',
263
+ params: params,
232
264
  }
233
265
  end
234
266
  end
@@ -6,15 +6,15 @@
6
6
  #
7
7
  # describe user('root') do
8
8
  # it { should exist }
9
- # its(:uid) { should eq 0 }
10
- # its(:gid) { should eq 0 }
11
- # its(:group) { should eq 'root' }
12
- # its(:groups) { should eq ['root', 'wheel']}
13
- # its(:home) { should eq '/root' }
14
- # its(:shell) { should eq '/bin/bash' }
15
- # its(:mindays) { should eq 0 }
16
- # its(:maxdays) { should eq 99 }
17
- # its(:warndays) { should eq 5 }
9
+ # its('uid') { should eq 0 }
10
+ # its('gid') { should eq 0 }
11
+ # its('group') { should eq 'root' }
12
+ # its('groups') { should eq ['root', 'wheel']}
13
+ # its('home') { should eq '/root' }
14
+ # its('shell') { should eq '/bin/bash' }
15
+ # its('mindays') { should eq 0 }
16
+ # its('maxdays') { should eq 99 }
17
+ # its('warndays') { should eq 5 }
18
18
  # end
19
19
  #
20
20
  # The following Serverspec matchers are deprecated in favor for direct value access
@@ -24,8 +24,8 @@
24
24
  # it { should have_uid 0 }
25
25
  # it { should have_home_directory '/root' }
26
26
  # it { should have_login_shell '/bin/bash' }
27
- # its(:minimum_days_between_password_change) { should eq 0 }
28
- # its(:maximum_days_between_password_change) { should eq 99 }
27
+ # its('minimum_days_between_password_change') { should eq 0 }
28
+ # its('maximum_days_between_password_change') { should eq 99 }
29
29
  # end
30
30
 
31
31
  # ServerSpec tests that are not supported:
@@ -119,13 +119,13 @@ module Inspec::Resources
119
119
 
120
120
  # implement 'mindays' method to be compatible with serverspec
121
121
  def minimum_days_between_password_change
122
- deprecated('minimum_days_between_password_change', "Please use 'its(:mindays)'")
122
+ deprecated('minimum_days_between_password_change', "Please use: its('mindays')")
123
123
  mindays
124
124
  end
125
125
 
126
126
  # implement 'maxdays' method to be compatible with serverspec
127
127
  def maximum_days_between_password_change
128
- deprecated('maximum_days_between_password_change', "Please use 'its(:maxdays)'")
128
+ deprecated('maximum_days_between_password_change', "Please use: its('maxdays')")
129
129
  maxdays
130
130
  end
131
131
 
@@ -137,12 +137,12 @@ module Inspec::Resources
137
137
  end
138
138
 
139
139
  def has_home_directory?(compare_home)
140
- deprecated('has_home_directory?', "Please use 'its(:home)'")
140
+ deprecated('has_home_directory?', "Please use: its('home')")
141
141
  home == compare_home
142
142
  end
143
143
 
144
144
  def has_login_shell?(compare_shell)
145
- deprecated('has_login_shell?', "Please use 'its(:shell)'")
145
+ deprecated('has_login_shell?', "Please use: its('shell')")
146
146
  shell == compare_shell
147
147
  end
148
148
 
@@ -45,8 +45,6 @@ module Inspec
45
45
  end
46
46
 
47
47
  def self.exec_options
48
- option :id, type: :string,
49
- desc: 'Attach a profile ID to all test results'
50
48
  target_options
51
49
  profile_options
52
50
  option :controls, type: :array,
@@ -68,7 +66,7 @@ module Inspec
68
66
  runner = Inspec::Runner.new(o)
69
67
  targets.each { |target| runner.add_target(target, opts) }
70
68
  exit runner.run
71
- rescue RuntimeError => e
69
+ rescue RuntimeError, Train::UserError => e
72
70
  $stderr.puts e.message
73
71
  exit 1
74
72
  end
@@ -95,15 +95,38 @@ module FilterTable
95
95
 
96
96
  private
97
97
 
98
+ def matches_float(x, y)
99
+ return false if x.nil?
100
+ return false if !x.is_a?(Float) && (x =~ /\A[-+]?(\d+\.?\d*|\.\d+)\z/).nil?
101
+ x.to_f == y
102
+ end
103
+
104
+ def matches_int(x, y)
105
+ return false if x.nil?
106
+ return false if !x.is_a?(Integer) && (x =~ /\A[-+]?\d+\z/).nil?
107
+ x.to_i == y
108
+ end
109
+
110
+ def matches_regex(x, y)
111
+ return x == y if x.is_a?(Regexp)
112
+ !x.to_s.match(y).nil?
113
+ end
114
+
115
+ def matches(x, y)
116
+ x === y # rubocop:disable Style/CaseEquality
117
+ end
118
+
98
119
  def filter_lines(table, field, condition)
120
+ m = case condition
121
+ when Float then method(:matches_float)
122
+ when Integer then method(:matches_int)
123
+ when Regexp then method(:matches_regex)
124
+ else method(:matches)
125
+ end
126
+
99
127
  table.find_all do |line|
100
128
  next unless line.key?(field)
101
- case line[field]
102
- when condition
103
- true
104
- else
105
- false
106
- end
129
+ m.call(line[field], condition)
107
130
  end
108
131
  end
109
132
  end
@@ -134,7 +157,7 @@ module FilterTable
134
157
  fields.each do |method, field_name|
135
158
  block = blocks[method]
136
159
  define_method method.to_sym do |condition = Show, &cond_block|
137
- return block.call(self) unless block.nil?
160
+ return block.call(self, condition) unless block.nil?
138
161
  return where(nil).get_fields(field_name) if condition == Show && !block_given?
139
162
  where({ field_name => condition }, &cond_block)
140
163
  end
@@ -1,6 +1,10 @@
1
1
  # encoding: utf-8
2
2
  # author: Stephan Renatus
3
3
 
4
+ directory '/etc/init' do
5
+ action :create
6
+ end
7
+
4
8
  file "/etc/init/upstart-running.conf" do
5
9
  content "exec tail -f /dev/null"
6
10
  end
@@ -20,6 +20,7 @@ module FunctionalHelper
20
20
  let(:examples_path) { File.join(repo_path, 'examples') }
21
21
 
22
22
  let(:example_profile) { File.join(examples_path, 'profile') }
23
+ let(:example_control) { File.join(example_profile, 'controls', 'example.rb') }
23
24
  let(:inheritance_profile) { File.join(examples_path, 'profile') }
24
25
 
25
26
  let(:dst) {
@@ -44,6 +44,6 @@ describe 'example inheritance profile' do
44
44
  s = out.stdout
45
45
  hm = JSON.load(s)
46
46
  hm['name'].must_equal 'inheritance'
47
- hm['rules'].length.must_equal 1 # TODO: flatten out or search deeper!
47
+ hm['controls'].length.must_equal 3
48
48
  end
49
49
  end
@@ -37,7 +37,8 @@ describe 'inspec compliance' do
37
37
 
38
38
  it 'inspec compliance profiles without authentication' do
39
39
  out = inspec('compliance profile')
40
- out.exit_status.must_equal 1
40
+ out.stdout.must_include 'You need to login first with `inspec compliance login`'
41
+ out.exit_status.must_equal 0
41
42
  end
42
43
 
43
44
  it 'try to upload a profile without directory' do
@@ -48,8 +49,8 @@ describe 'inspec compliance' do
48
49
 
49
50
  it 'try to upload a profile a non-existing path' do
50
51
  out = inspec('compliance upload /path/to/dir')
51
- out.stdout.must_include 'Directory /path/to/dir does not exist.'
52
- out.exit_status.must_equal 1
52
+ out.stdout.must_include 'You need to login first with `inspec compliance login`'
53
+ out.exit_status.must_equal 0
53
54
  end
54
55
 
55
56
  it 'logout' do
@@ -0,0 +1,122 @@
1
+ # encoding: utf-8
2
+ # author: Dominik Richter
3
+ # author: Christoph Hartmann
4
+
5
+ require 'functional/helper'
6
+
7
+ describe 'inspec exec with json formatter' do
8
+ include FunctionalHelper
9
+
10
+ it 'can execute a simple file with the json formatter' do
11
+ out = inspec('exec ' + example_control + ' --format json')
12
+ out.stderr.must_equal ''
13
+ out.exit_status.must_equal 0
14
+ JSON.load(out.stdout).must_be_kind_of Hash
15
+ end
16
+
17
+ it 'can execute the profile with the json formatter' do
18
+ out = inspec('exec ' + example_profile + ' --format json')
19
+ out.stderr.must_equal ''
20
+ out.exit_status.must_equal 0
21
+ JSON.load(out.stdout).must_be_kind_of Hash
22
+ end
23
+
24
+ describe 'execute a profile with json formatting' do
25
+ let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format json').stdout) }
26
+ let(:profile) { json['profiles']['profile'] }
27
+ let(:controls) { profile['controls'] }
28
+ let(:ex1) { controls['tmp-1.0'] }
29
+ let(:ex2) {
30
+ k = controls.keys.find { |x| x =~ /generated/ }
31
+ controls[k]
32
+ }
33
+ let(:ex3) { profile['controls']['gordon-1.0'] }
34
+ let(:check_result) {
35
+ ex3['results'].find { |x| x['resource'] == 'gordon_config' }
36
+ }
37
+
38
+ it 'has all the metadata' do
39
+ actual = profile.dup
40
+ key = actual.delete('controls').keys
41
+ .find { |x| x =~ /generated from example.rb/ }
42
+
43
+ actual.must_equal({
44
+ "name" => "profile",
45
+ "title" => "InSpec Example Profile",
46
+ "maintainer" => "Chef Software, Inc.",
47
+ "copyright" => "Chef Software, Inc.",
48
+ "copyright_email" => "support@chef.io",
49
+ "license" => "Apache 2 license",
50
+ "summary" => "Demonstrates the use of InSpec Compliance Profile",
51
+ "version" => "1.0.0",
52
+ "supports" => [{"os-family" => "unix"}],
53
+ "groups" => {
54
+ "controls/meta.rb" => {"title"=>"SSH Server Configuration", "controls"=>["ssh-1"]},
55
+ "controls/example.rb" => {"title"=>"/tmp profile", "controls"=>["tmp-1.0", key]},
56
+ "controls/gordon.rb" => {"title"=>"Gordon Config Checks", "controls"=>["gordon-1.0"]},
57
+ },
58
+ })
59
+ end
60
+
61
+ it 'must have 4 controls' do
62
+ controls.length.must_equal 4
63
+ end
64
+
65
+ it 'has an id for every control' do
66
+ controls.keys.find(&:nil?).must_be :nil?
67
+ end
68
+
69
+ it 'has no missing checks' do
70
+ json['other_checks'].must_equal([])
71
+ end
72
+
73
+ it 'has results for every control' do
74
+ ex1['results'].length.must_equal 1
75
+ ex2['results'].length.must_equal 1
76
+ ex3['results'].length.must_equal 2
77
+ end
78
+
79
+ it 'has the right result for tmp-1.0' do
80
+ actual = ex1.dup
81
+
82
+ src = actual.delete('source_location')
83
+ src[0].must_match %r{examples/profile/controls/example.rb$}
84
+ src[1].must_equal 8
85
+
86
+ result = actual.delete('results')[0]
87
+ result.wont_be :nil?
88
+ result['status'].must_equal 'passed'
89
+ result['code_desc'].must_equal 'File /tmp should be directory'
90
+ result['run_time'].wont_be :nil?
91
+ result['start_time'].wont_be :nil?
92
+
93
+ actual.must_equal({
94
+ "title" => "Create /tmp directory",
95
+ "desc" => "An optional description...",
96
+ "impact" => 0.7,
97
+ "refs" => [
98
+ {
99
+ "url" => "http://...",
100
+ "ref" => "Document A-12"
101
+ }
102
+ ],
103
+ "tags" => {
104
+ "data" => "temp data",
105
+ "security" => nil
106
+ },
107
+ "code" => "control \"tmp-1.0\" do # A unique ID for this control\n impact 0.7 # The criticality, if this control fails.\n title \"Create /tmp directory\" # A human-readable title\n desc \"An optional description...\" # Describe why this is needed\n tag data: \"temp data\" # A tag allows you to associate key information\n tag \"security\" # to the test\n ref \"Document A-12\", url: 'http://...' # Additional references\n\n describe file('/tmp') do # The actual test\n it { should be_directory }\n end\nend\n",
108
+ })
109
+ end
110
+ end
111
+
112
+ describe 'with a profile that is not supported on this OS/platform' do
113
+ let(:out) { inspec('exec ' + File.join(profile_path, 'skippy-profile-os') + ' --format json') }
114
+ let(:json) { JSON.load(out.stdout) }
115
+
116
+ # TODO: failure handling in json formatters...
117
+
118
+ it 'never runs the actual resource' do
119
+ File.exist?('/tmp/inspec_test_DONT_CREATE').must_equal false
120
+ end
121
+ end
122
+ end