inspec 0.12.0 → 0.14.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.
- 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
|