inspec 0.12.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -2
- data/bin/inspec +11 -9
- data/docs/matchers.rst +129 -0
- data/docs/resources.rst +64 -37
- data/inspec.gemspec +1 -1
- data/lib/bundles/inspec-compliance/cli.rb +1 -1
- data/lib/bundles/inspec-compliance/configuration.rb +1 -0
- data/lib/bundles/inspec-compliance/target.rb +16 -32
- data/lib/bundles/inspec-init/cli.rb +2 -0
- data/lib/bundles/inspec-supermarket.rb +13 -0
- data/lib/bundles/inspec-supermarket/api.rb +2 -0
- data/lib/bundles/inspec-supermarket/cli.rb +2 -2
- data/lib/bundles/inspec-supermarket/target.rb +11 -15
- data/lib/fetchers/local.rb +31 -0
- data/lib/fetchers/tar.rb +48 -0
- data/lib/fetchers/url.rb +100 -0
- data/lib/fetchers/zip.rb +47 -0
- data/lib/inspec.rb +2 -3
- data/lib/inspec/fetcher.rb +22 -0
- data/lib/inspec/metadata.rb +4 -2
- data/lib/inspec/plugins.rb +2 -0
- data/lib/inspec/plugins/fetcher.rb +97 -0
- data/lib/inspec/plugins/source_reader.rb +36 -0
- data/lib/inspec/profile.rb +92 -81
- data/lib/inspec/resource.rb +1 -0
- data/lib/inspec/runner.rb +15 -35
- data/lib/inspec/source_reader.rb +32 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +5 -6
- data/lib/resources/file.rb +8 -2
- data/lib/resources/passwd.rb +71 -45
- data/lib/resources/service.rb +13 -9
- data/lib/resources/shadow.rb +135 -0
- data/lib/source_readers/flat.rb +38 -0
- data/lib/source_readers/inspec.rb +78 -0
- data/lib/utils/base_cli.rb +2 -2
- data/lib/utils/parser.rb +1 -1
- data/lib/utils/plugin_registry.rb +93 -0
- data/test/docker_test.rb +1 -1
- data/test/helper.rb +62 -2
- data/test/integration/cookbooks/os_prepare/recipes/service.rb +4 -2
- data/test/integration/test/integration/default/compare_matcher_spec.rb +11 -0
- data/test/integration/test/integration/default/service_spec.rb +16 -1
- data/test/unit/fetchers.rb +61 -0
- data/test/unit/fetchers/local_test.rb +67 -0
- data/test/unit/fetchers/tar_test.rb +36 -0
- data/test/unit/fetchers/url_test.rb +152 -0
- data/test/unit/fetchers/zip_test.rb +36 -0
- data/test/unit/mock/files/passwd +1 -1
- data/test/unit/mock/files/shadow +2 -0
- data/test/unit/mock/profiles/complete-profile/libraries/testlib.rb +1 -0
- data/test/unit/plugin_test.rb +0 -1
- data/test/unit/profile_test.rb +32 -53
- data/test/unit/resources/passwd_test.rb +69 -14
- data/test/unit/resources/shadow_test.rb +67 -0
- data/test/unit/source_reader_test.rb +17 -0
- data/test/unit/source_readers/flat_test.rb +61 -0
- data/test/unit/source_readers/inspec_test.rb +38 -0
- data/test/unit/utils/passwd_parser_test.rb +1 -1
- metadata +40 -21
- data/lib/inspec/targets.rb +0 -10
- data/lib/inspec/targets/archive.rb +0 -33
- data/lib/inspec/targets/core.rb +0 -56
- data/lib/inspec/targets/dir.rb +0 -144
- data/lib/inspec/targets/file.rb +0 -33
- data/lib/inspec/targets/folder.rb +0 -38
- data/lib/inspec/targets/tar.rb +0 -61
- data/lib/inspec/targets/url.rb +0 -78
- data/lib/inspec/targets/zip.rb +0 -55
- data/test/unit/targets.rb +0 -132
@@ -0,0 +1,32 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# author: Dominik Richter
|
3
|
+
# author: Christoph Hartmann
|
4
|
+
|
5
|
+
require 'inspec/plugins'
|
6
|
+
require 'utils/plugin_registry'
|
7
|
+
|
8
|
+
module Inspec
|
9
|
+
# Pre-checking of target resolution. Make sure that SourceReader plugins
|
10
|
+
# always receive a fetcher.
|
11
|
+
class SourceReaderRegistry < PluginRegistry
|
12
|
+
def resolve(target)
|
13
|
+
return nil if target.nil?
|
14
|
+
unless target.is_a? Inspec::Plugins::Fetcher
|
15
|
+
fail "SourceReader cannot resolve targets that aren't Fetchers: #{target.class}"
|
16
|
+
end
|
17
|
+
super(target)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
SourceReader = SourceReaderRegistry.new
|
22
|
+
|
23
|
+
def self.source_reader(version)
|
24
|
+
if version != 1
|
25
|
+
fail 'Only source readers version 1 is supported!'
|
26
|
+
end
|
27
|
+
Inspec::Plugins::SourceReader
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require 'source_readers/inspec'
|
32
|
+
require 'source_readers/flat'
|
data/lib/inspec/version.rb
CHANGED
data/lib/matchers/matchers.rb
CHANGED
@@ -229,23 +229,22 @@ end
|
|
229
229
|
RSpec::Matchers.define :cmp do |expected|
|
230
230
|
|
231
231
|
def integer?(value)
|
232
|
-
|
233
|
-
false
|
232
|
+
!(value =~ /\A\d+\Z/).nil?
|
234
233
|
end
|
235
234
|
|
236
235
|
def float?(value)
|
237
|
-
|
238
|
-
|
236
|
+
Float(value)
|
237
|
+
true
|
239
238
|
rescue ArgumentError => _ex
|
240
239
|
false
|
241
240
|
end
|
242
241
|
|
243
242
|
def octal?(value)
|
244
|
-
|
245
|
-
false
|
243
|
+
!(value =~ /\A0+\d+\Z/).nil?
|
246
244
|
end
|
247
245
|
|
248
246
|
match do |actual|
|
247
|
+
actual = actual[0] if actual.is_a?(Array) && !expected.is_a?(Array) && actual.length == 1
|
249
248
|
# if actual and expected are strings
|
250
249
|
if expected.is_a?(String) && actual.is_a?(String)
|
251
250
|
actual.casecmp(expected) == 0
|
data/lib/resources/file.rb
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
# license: All rights reserved
|
6
6
|
|
7
7
|
module Inspec::Resources
|
8
|
-
class File < Inspec.resource(1)
|
8
|
+
class File < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
9
9
|
name 'file'
|
10
10
|
desc 'Use the file InSpec audit resource to test all system file types, including files, directories, symbolic links, named pipes, sockets, character devices, block devices, and doors.'
|
11
11
|
example "
|
@@ -29,7 +29,7 @@ module Inspec::Resources
|
|
29
29
|
%w{
|
30
30
|
type exist? file? block_device? character_device? socket? directory?
|
31
31
|
symlink? pipe? mode mode? owner owned_by? group grouped_into? link_target
|
32
|
-
link_path linked_to?
|
32
|
+
link_path linked_to? mtime size selinux_label immutable?
|
33
33
|
product_version file_version version? md5sum sha256sum
|
34
34
|
}.each do |m|
|
35
35
|
define_method m.to_sym do |*args|
|
@@ -37,6 +37,12 @@ module Inspec::Resources
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
+
def content
|
41
|
+
res = file.content
|
42
|
+
return nil if res.nil?
|
43
|
+
res.force_encoding('utf-8')
|
44
|
+
end
|
45
|
+
|
40
46
|
def contain(*_)
|
41
47
|
fail 'Contain is not supported. Please use standard RSpec matchers.'
|
42
48
|
end
|
data/lib/resources/passwd.rb
CHANGED
@@ -13,88 +13,114 @@
|
|
13
13
|
# - home directory
|
14
14
|
# - command
|
15
15
|
|
16
|
-
# usage:
|
17
|
-
#
|
18
|
-
# describe passwd do
|
19
|
-
# its(:usernames) { should eq ['root'] }
|
20
|
-
# its(:uids) { should eq [0] }
|
21
|
-
# end
|
22
|
-
#
|
23
|
-
# describe passwd.uid(0) do
|
24
|
-
# its(:username) { should eq 'root' }
|
25
|
-
# its(:count) { should eq 1 }
|
26
|
-
# end
|
27
|
-
|
28
16
|
require 'utils/parser'
|
29
17
|
|
30
18
|
class Passwd < Inspec.resource(1)
|
31
19
|
name 'passwd'
|
32
20
|
desc 'Use the passwd InSpec audit resource to test the contents of /etc/passwd, which contains the following information for users that may log into the system and/or as users that own running processes.'
|
33
21
|
example "
|
34
|
-
describe passwd
|
35
|
-
its('
|
22
|
+
describe passwd do
|
23
|
+
its('users') { should_not include 'forbidden_user' }
|
24
|
+
end
|
25
|
+
|
26
|
+
describe passwd.uids(0) do
|
27
|
+
its('users') { should cmp 'root' }
|
36
28
|
its('count') { should eq 1 }
|
37
29
|
end
|
30
|
+
|
31
|
+
describe passwd.shells(/nologin/) do
|
32
|
+
# find all users with a nologin shell
|
33
|
+
its('users') { should_not include 'my_login_user' }
|
34
|
+
end
|
38
35
|
"
|
39
36
|
|
40
37
|
include PasswdParser
|
41
38
|
|
42
39
|
attr_reader :uid
|
43
|
-
attr_reader :
|
40
|
+
attr_reader :params
|
41
|
+
attr_reader :content
|
42
|
+
attr_reader :lines
|
44
43
|
|
45
|
-
def initialize(path = nil)
|
44
|
+
def initialize(path = nil, opts = nil)
|
45
|
+
opts ||= {}
|
46
46
|
@path = path || '/etc/passwd'
|
47
|
-
@content = inspec.file(@path).content
|
48
|
-
@
|
47
|
+
@content = opts[:content] || inspec.file(@path).content
|
48
|
+
@lines = @content.to_s.split("\n")
|
49
|
+
@filters = opts[:filters] || ''
|
50
|
+
@params = parse_passwd(@content)
|
49
51
|
end
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
53
|
+
def filter(hm = {})
|
54
|
+
return self if hm.nil? || hm.empty?
|
55
|
+
res = @params
|
56
|
+
filters = ''
|
57
|
+
hm.each do |attr, condition|
|
58
|
+
condition = condition.to_s if condition.is_a? Integer
|
59
|
+
filters += " #{attr} = #{condition.inspect}"
|
60
|
+
res = res.find_all do |line|
|
61
|
+
case line[attr.to_s]
|
62
|
+
when condition
|
63
|
+
true
|
64
|
+
else
|
65
|
+
false
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
content = res.map { |x| x.values.join(':') }.join("\n")
|
70
|
+
Passwd.new(@path, content: content, filters: @filters + filters)
|
55
71
|
end
|
56
72
|
|
57
73
|
def usernames
|
58
|
-
|
74
|
+
warn '[DEPRECATION] `passwd.usernames` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
|
75
|
+
users
|
59
76
|
end
|
60
77
|
|
61
|
-
def
|
62
|
-
|
78
|
+
def username
|
79
|
+
warn '[DEPRECATION] `passwd.user` is deprecated. Please use `passwd.users` instead. It will be removed in version 1.0.0.'
|
80
|
+
users[0]
|
63
81
|
end
|
64
82
|
|
65
|
-
def
|
66
|
-
|
83
|
+
def uid(x)
|
84
|
+
warn '[DEPRECATION] `passwd.uid(arg)` is deprecated. Please use `passwd.uids(arg)` instead. It will be removed in version 1.0.0.'
|
85
|
+
uids(x)
|
67
86
|
end
|
68
87
|
|
69
|
-
def
|
70
|
-
map_data('
|
88
|
+
def users(name = nil)
|
89
|
+
name.nil? ? map_data('user') : filter(user: name)
|
71
90
|
end
|
72
91
|
|
73
|
-
def
|
74
|
-
'
|
92
|
+
def passwords(password = nil)
|
93
|
+
password.nil? ? map_data('password') : filter(password: password)
|
75
94
|
end
|
76
95
|
|
77
|
-
|
96
|
+
def uids(uid = nil)
|
97
|
+
uid.nil? ? map_data('uid') : filter(uid: uid)
|
98
|
+
end
|
78
99
|
|
79
|
-
def
|
80
|
-
|
81
|
-
x[id]
|
82
|
-
}
|
100
|
+
def gids(gid = nil)
|
101
|
+
gid.nil? ? map_data('gid') : filter(gid: gid)
|
83
102
|
end
|
84
|
-
end
|
85
103
|
|
86
|
-
|
87
|
-
|
88
|
-
def initialize(passwd, uid)
|
89
|
-
@passwd = passwd
|
90
|
-
@users = @passwd.parsed.select { |x| x['uid'] == uid.to_s }
|
104
|
+
def homes(home = nil)
|
105
|
+
home.nil? ? map_data('home') : filter(home: home)
|
91
106
|
end
|
92
107
|
|
93
|
-
def
|
94
|
-
|
108
|
+
def shells(shell = nil)
|
109
|
+
shell.nil? ? map_data('shell') : filter(shell: shell)
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_s
|
113
|
+
f = @filters.empty? ? '' : ' with'+@filters
|
114
|
+
"/etc/passwd#{f}"
|
95
115
|
end
|
96
116
|
|
97
117
|
def count
|
98
|
-
@
|
118
|
+
@params.length
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def map_data(id)
|
124
|
+
@params.map { |x| x[id] }
|
99
125
|
end
|
100
126
|
end
|
data/lib/resources/service.rb
CHANGED
@@ -189,7 +189,7 @@ end
|
|
189
189
|
# @see: http://www.freedesktop.org/software/systemd/man/systemd-system.conf.html
|
190
190
|
class Systemd < ServiceManager
|
191
191
|
def initialize(inspec, service_ctl = nil)
|
192
|
-
@service_ctl
|
192
|
+
@service_ctl = service_ctl || 'systemctl'
|
193
193
|
super
|
194
194
|
end
|
195
195
|
|
@@ -270,7 +270,7 @@ end
|
|
270
270
|
# @see: http://upstart.ubuntu.com
|
271
271
|
class Upstart < ServiceManager
|
272
272
|
def initialize(service_name, service_ctl = nil)
|
273
|
-
@service_ctl
|
273
|
+
@service_ctl = service_ctl || 'initctl'
|
274
274
|
super
|
275
275
|
end
|
276
276
|
|
@@ -311,6 +311,8 @@ class Upstart < ServiceManager
|
|
311
311
|
config = inspec.file("/etc/init/#{service_name}.conf").content
|
312
312
|
end
|
313
313
|
|
314
|
+
# disregard if the config does not exist
|
315
|
+
return nil if config.nil?
|
314
316
|
enabled = !config[/^\s*start on/].nil?
|
315
317
|
|
316
318
|
# implement fallback for Ubuntu 10.04
|
@@ -325,8 +327,10 @@ class Upstart < ServiceManager
|
|
325
327
|
end
|
326
328
|
|
327
329
|
def version
|
328
|
-
@version ||=
|
329
|
-
|
330
|
+
@version ||= (
|
331
|
+
out = inspec.command("#{service_ctl} --version").stdout
|
332
|
+
Gem::Version.new(out[/\(upstart ([^\)]+)\)/, 1])
|
333
|
+
)
|
330
334
|
end
|
331
335
|
end
|
332
336
|
|
@@ -334,7 +338,7 @@ class SysV < ServiceManager
|
|
334
338
|
RUNLEVELS = { 0=>false, 1=>false, 2=>false, 3=>false, 4=>false, 5=>false, 6=>false }.freeze
|
335
339
|
|
336
340
|
def initialize(service_name, service_ctl = nil)
|
337
|
-
@service_ctl
|
341
|
+
@service_ctl = service_ctl || 'service'
|
338
342
|
super
|
339
343
|
end
|
340
344
|
|
@@ -386,7 +390,7 @@ end
|
|
386
390
|
# @see: https://www.freebsd.org/cgi/man.cgi?query=rc.conf&sektion=5
|
387
391
|
class BSDInit < ServiceManager
|
388
392
|
def initialize(service_name, service_ctl = nil)
|
389
|
-
@service_ctl
|
393
|
+
@service_ctl = service_ctl || 'service'
|
390
394
|
super
|
391
395
|
end
|
392
396
|
|
@@ -423,7 +427,7 @@ end
|
|
423
427
|
|
424
428
|
class Runit < ServiceManager
|
425
429
|
def initialize(service_name, service_ctl = nil)
|
426
|
-
@service_ctl
|
430
|
+
@service_ctl = service_ctl || 'sv'
|
427
431
|
super
|
428
432
|
end
|
429
433
|
|
@@ -452,7 +456,7 @@ end
|
|
452
456
|
# new launctl on macos 10.10
|
453
457
|
class LaunchCtl < ServiceManager
|
454
458
|
def initialize(service_name, service_ctl = nil)
|
455
|
-
@service_ctl
|
459
|
+
@service_ctl = service_ctl || 'launchctl'
|
456
460
|
super
|
457
461
|
end
|
458
462
|
|
@@ -562,7 +566,7 @@ end
|
|
562
566
|
# Solaris services
|
563
567
|
class Svcs < ServiceManager
|
564
568
|
def initialize(service_name, service_ctl = nil)
|
565
|
-
@service_ctl
|
569
|
+
@service_ctl = service_ctl || 'svcs'
|
566
570
|
super
|
567
571
|
end
|
568
572
|
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# copyright: 2016, Chef Software Inc.
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'forwardable'
|
7
|
+
|
8
|
+
# The file format consists of
|
9
|
+
# - user
|
10
|
+
# - password
|
11
|
+
# - last_change
|
12
|
+
# - min_days before password change
|
13
|
+
# - max_days until password change
|
14
|
+
# - warn_days before warning about expiry
|
15
|
+
# - inactive_days before deactivating the account
|
16
|
+
# - expiry_date when this account will expire
|
17
|
+
|
18
|
+
class Shadow < Inspec.resource(1)
|
19
|
+
name 'shadow'
|
20
|
+
desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
|
21
|
+
'which contains the following information for users that may log into '\
|
22
|
+
'the system and/or as users that own running processes.'
|
23
|
+
example "
|
24
|
+
describe shadow do
|
25
|
+
its('users') { should_not include 'forbidden_user' }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe shadow.users('bin') do
|
29
|
+
its('password') { should cmp 'x' }
|
30
|
+
its('count') { should eq 1 }
|
31
|
+
end
|
32
|
+
"
|
33
|
+
|
34
|
+
extend Forwardable
|
35
|
+
attr_reader :params
|
36
|
+
attr_reader :content
|
37
|
+
attr_reader :lines
|
38
|
+
|
39
|
+
def initialize(path = '/etc/shadow', opts = nil)
|
40
|
+
opts ||= {}
|
41
|
+
@path = path || '/etc/shadow'
|
42
|
+
@content = opts[:content] || inspec.file(@path).content
|
43
|
+
@lines = @content.to_s.split("\n")
|
44
|
+
@filters = opts[:filters] || ''
|
45
|
+
@params = @lines.map { |l| parse_shadow_line(l) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def filter(hm = {})
|
49
|
+
return self if hm.nil? || hm.empty?
|
50
|
+
res = @params
|
51
|
+
filters = ''
|
52
|
+
hm.each do |attr, condition|
|
53
|
+
condition = condition.to_s if condition.is_a? Integer
|
54
|
+
filters += " #{attr} = #{condition.inspect}"
|
55
|
+
res = res.find_all do |line|
|
56
|
+
case line[attr.to_s]
|
57
|
+
when condition
|
58
|
+
true
|
59
|
+
else
|
60
|
+
false
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
content = res.map { |x| x.values.join(':') }.join("\n")
|
65
|
+
Shadow.new(@path, content: content, filters: @filters + filters)
|
66
|
+
end
|
67
|
+
|
68
|
+
def entries
|
69
|
+
@lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def users(name = nil)
|
73
|
+
name.nil? ? map_data('user') : filter(user: name)
|
74
|
+
end
|
75
|
+
|
76
|
+
def passwords(password = nil)
|
77
|
+
password.nil? ? map_data('password') : filter(password: password)
|
78
|
+
end
|
79
|
+
|
80
|
+
def last_changes(filter_by = nil)
|
81
|
+
filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
|
82
|
+
end
|
83
|
+
|
84
|
+
def min_days(filter_by = nil)
|
85
|
+
filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
|
86
|
+
end
|
87
|
+
|
88
|
+
def max_days(filter_by = nil)
|
89
|
+
filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
|
90
|
+
end
|
91
|
+
|
92
|
+
def warn_days(filter_by = nil)
|
93
|
+
filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
|
94
|
+
end
|
95
|
+
|
96
|
+
def inactive_days(filter_by = nil)
|
97
|
+
filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
|
98
|
+
end
|
99
|
+
|
100
|
+
def expiry_dates(filter_by = nil)
|
101
|
+
filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_s
|
105
|
+
f = @filters.empty? ? '' : ' with'+@filters
|
106
|
+
"/etc/shadow#{f}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def_delegator :@params, :length, :count
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def map_data(id)
|
114
|
+
@params.map { |x| x[id] }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Parse a line of /etc/shadow
|
118
|
+
#
|
119
|
+
# @param [String] line a line of /etc/shadow
|
120
|
+
# @return [Hash] Map of entries in this line
|
121
|
+
def parse_shadow_line(line)
|
122
|
+
x = line.split(':')
|
123
|
+
{
|
124
|
+
'user' => x.at(0),
|
125
|
+
'password' => x.at(1),
|
126
|
+
'last_change' => x.at(2),
|
127
|
+
'min_days' => x.at(3),
|
128
|
+
'max_days' => x.at(4),
|
129
|
+
'warn_days' => x.at(5),
|
130
|
+
'inactive_days' => x.at(6),
|
131
|
+
'expiry_date' => x.at(7),
|
132
|
+
'reserved' => x.at(8),
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|