inspec 0.20.1 → 0.21.0

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