inspec 0.14.8 → 0.15.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 +25 -2
- data/bin/inspec +3 -4
- data/examples/inheritance/README.md +19 -0
- data/examples/inheritance/controls/example.rb +11 -0
- data/examples/inheritance/inspec.yml +10 -0
- data/lib/bundles/inspec-compliance/cli.rb +1 -4
- data/lib/bundles/inspec-supermarket/cli.rb +1 -4
- data/lib/inspec/dsl.rb +48 -55
- data/lib/inspec/profile.rb +6 -2
- data/lib/inspec/profile_context.rb +21 -8
- data/lib/inspec/runner.rb +17 -12
- data/lib/inspec/runner_rspec.rb +1 -0
- data/lib/inspec/version.rb +1 -1
- data/lib/resources/apache.rb +20 -18
- data/lib/resources/apache_conf.rb +92 -90
- data/lib/resources/apt.rb +92 -90
- data/lib/resources/audit_policy.rb +35 -33
- data/lib/resources/auditd_conf.rb +41 -39
- data/lib/resources/auditd_rules.rb +155 -153
- data/lib/resources/bond.rb +1 -1
- data/lib/resources/bridge.rb +97 -95
- data/lib/resources/command.rb +47 -45
- data/lib/resources/csv.rb +23 -21
- data/lib/resources/directory.rb +1 -1
- data/lib/resources/etc_group.rb +116 -114
- data/lib/resources/file.rb +1 -1
- data/lib/resources/gem.rb +39 -37
- data/lib/resources/group.rb +100 -98
- data/lib/resources/host.rb +103 -101
- data/lib/resources/inetd_conf.rb +42 -40
- data/lib/resources/ini.rb +15 -13
- data/lib/resources/interface.rb +106 -104
- data/lib/resources/iptables.rb +36 -34
- data/lib/resources/json.rb +64 -62
- data/lib/resources/kernel_module.rb +30 -28
- data/lib/resources/kernel_parameter.rb +44 -42
- data/lib/resources/limits_conf.rb +41 -39
- data/lib/resources/login_def.rb +38 -36
- data/lib/resources/mount.rb +43 -41
- data/lib/resources/mysql.rb +67 -65
- data/lib/resources/mysql_conf.rb +89 -87
- data/lib/resources/mysql_session.rb +46 -44
- data/lib/resources/npm.rb +35 -33
- data/lib/resources/ntp_conf.rb +44 -42
- data/lib/resources/oneget.rb +46 -44
- data/lib/resources/os.rb +22 -20
- data/lib/resources/os_env.rb +47 -45
- data/lib/resources/package.rb +213 -211
- data/lib/resources/parse_config.rb +59 -57
- data/lib/resources/passwd.rb +89 -87
- data/lib/resources/pip.rb +60 -58
- data/lib/resources/port.rb +352 -350
- data/lib/resources/postgres.rb +26 -24
- data/lib/resources/postgres_conf.rb +66 -64
- data/lib/resources/postgres_session.rb +47 -45
- data/lib/resources/processes.rb +56 -54
- data/lib/resources/registry_key.rb +150 -148
- data/lib/resources/script.rb +30 -28
- data/lib/resources/security_policy.rb +56 -54
- data/lib/resources/service.rb +638 -636
- data/lib/resources/shadow.rb +98 -96
- data/lib/resources/ssh_conf.rb +58 -56
- data/lib/resources/user.rb +363 -361
- data/lib/resources/windows_feature.rb +46 -44
- data/lib/resources/xinetd.rb +111 -109
- data/lib/resources/yaml.rb +16 -14
- data/lib/resources/yum.rb +107 -105
- data/lib/utils/base_cli.rb +18 -0
- data/test/helper.rb +2 -2
- data/test/unit/profile_context_test.rb +1 -1
- data/test/unit/resources/file_test.rb +1 -1
- data/test/unit/resources/mount_test.rb +1 -1
- metadata +5 -2
data/lib/resources/shadow.rb
CHANGED
@@ -15,121 +15,123 @@ require 'forwardable'
|
|
15
15
|
# - inactive_days before deactivating the account
|
16
16
|
# - expiry_date when this account will expire
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
18
|
+
module Inspec::Resources
|
19
|
+
class Shadow < Inspec.resource(1)
|
20
|
+
name 'shadow'
|
21
|
+
desc 'Use the shadow InSpec resource to test the contents of /etc/shadow, '\
|
22
|
+
'which contains the following information for users that may log into '\
|
23
|
+
'the system and/or as users that own running processes.'
|
24
|
+
example "
|
25
|
+
describe shadow do
|
26
|
+
its('users') { should_not include 'forbidden_user' }
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
describe shadow.users('bin') do
|
30
|
+
its('password') { should cmp 'x' }
|
31
|
+
its('count') { should eq 1 }
|
32
|
+
end
|
33
|
+
"
|
34
|
+
|
35
|
+
extend Forwardable
|
36
|
+
attr_reader :params
|
37
|
+
attr_reader :content
|
38
|
+
attr_reader :lines
|
39
|
+
|
40
|
+
def initialize(path = '/etc/shadow', opts = nil)
|
41
|
+
opts ||= {}
|
42
|
+
@path = path || '/etc/shadow'
|
43
|
+
@content = opts[:content] || inspec.file(@path).content
|
44
|
+
@lines = @content.to_s.split("\n")
|
45
|
+
@filters = opts[:filters] || ''
|
46
|
+
@params = @lines.map { |l| parse_shadow_line(l) }
|
31
47
|
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
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
49
|
+
def filter(hm = {})
|
50
|
+
return self if hm.nil? || hm.empty?
|
51
|
+
res = @params
|
52
|
+
filters = ''
|
53
|
+
hm.each do |attr, condition|
|
54
|
+
condition = condition.to_s if condition.is_a? Integer
|
55
|
+
filters += " #{attr} = #{condition.inspect}"
|
56
|
+
res = res.find_all do |line|
|
57
|
+
case line[attr.to_s]
|
58
|
+
when condition
|
59
|
+
true
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
61
63
|
end
|
62
64
|
end
|
65
|
+
content = res.map { |x| x.values.join(':') }.join("\n")
|
66
|
+
Shadow.new(@path, content: content, filters: @filters + filters)
|
63
67
|
end
|
64
|
-
content = res.map { |x| x.values.join(':') }.join("\n")
|
65
|
-
Shadow.new(@path, content: content, filters: @filters + filters)
|
66
|
-
end
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
def entries
|
70
|
+
@lines.map { |line| Shadow.new(@path, content: line, filters: @filters) }
|
71
|
+
end
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
73
|
+
def users(name = nil)
|
74
|
+
name.nil? ? map_data('user') : filter(user: name)
|
75
|
+
end
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
77
|
+
def passwords(password = nil)
|
78
|
+
password.nil? ? map_data('password') : filter(password: password)
|
79
|
+
end
|
79
80
|
|
80
|
-
|
81
|
-
|
82
|
-
|
81
|
+
def last_changes(filter_by = nil)
|
82
|
+
filter_by.nil? ? map_data('last_change') : filter(last_change: filter_by)
|
83
|
+
end
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
def min_days(filter_by = nil)
|
86
|
+
filter_by.nil? ? map_data('min_days') : filter(min_days: filter_by)
|
87
|
+
end
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
89
|
+
def max_days(filter_by = nil)
|
90
|
+
filter_by.nil? ? map_data('max_days') : filter(max_days: filter_by)
|
91
|
+
end
|
91
92
|
|
92
|
-
|
93
|
-
|
94
|
-
|
93
|
+
def warn_days(filter_by = nil)
|
94
|
+
filter_by.nil? ? map_data('warn_days') : filter(warn_days: filter_by)
|
95
|
+
end
|
95
96
|
|
96
|
-
|
97
|
-
|
98
|
-
|
97
|
+
def inactive_days(filter_by = nil)
|
98
|
+
filter_by.nil? ? map_data('inactive_days') : filter(inactive_days: filter_by)
|
99
|
+
end
|
99
100
|
|
100
|
-
|
101
|
-
|
102
|
-
|
101
|
+
def expiry_dates(filter_by = nil)
|
102
|
+
filter_by.nil? ? map_data('expiry_date') : filter(expiry_date: filter_by)
|
103
|
+
end
|
103
104
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
105
|
+
def to_s
|
106
|
+
f = @filters.empty? ? '' : ' with'+@filters
|
107
|
+
"/etc/shadow#{f}"
|
108
|
+
end
|
108
109
|
|
109
|
-
|
110
|
+
def_delegator :@params, :length, :count
|
110
111
|
|
111
|
-
|
112
|
+
private
|
112
113
|
|
113
|
-
|
114
|
-
|
115
|
-
|
114
|
+
def map_data(id)
|
115
|
+
@params.map { |x| x[id] }
|
116
|
+
end
|
116
117
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
118
|
+
# Parse a line of /etc/shadow
|
119
|
+
#
|
120
|
+
# @param [String] line a line of /etc/shadow
|
121
|
+
# @return [Hash] Map of entries in this line
|
122
|
+
def parse_shadow_line(line)
|
123
|
+
x = line.split(':')
|
124
|
+
{
|
125
|
+
'user' => x.at(0),
|
126
|
+
'password' => x.at(1),
|
127
|
+
'last_change' => x.at(2),
|
128
|
+
'min_days' => x.at(3),
|
129
|
+
'max_days' => x.at(4),
|
130
|
+
'warn_days' => x.at(5),
|
131
|
+
'inactive_days' => x.at(6),
|
132
|
+
'expiry_date' => x.at(7),
|
133
|
+
'reserved' => x.at(8),
|
134
|
+
}
|
135
|
+
end
|
134
136
|
end
|
135
137
|
end
|
data/lib/resources/ssh_conf.rb
CHANGED
@@ -6,76 +6,78 @@
|
|
6
6
|
|
7
7
|
require 'utils/simpleconfig'
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
module Inspec::Resources
|
10
|
+
class SshConf < Inspec.resource(1)
|
11
|
+
name 'ssh_config'
|
12
|
+
desc 'Use the sshd_config InSpec audit resource to test configuration data for the Open SSH daemon located at /etc/ssh/sshd_config on Linux and UNIX platforms. sshd---the Open SSH daemon---listens on dedicated ports, starts a daemon for each incoming connection, and then handles encryption, authentication, key exchanges, command executation, and data exchanges.'
|
13
|
+
example "
|
14
|
+
describe sshd_config do
|
15
|
+
its('Protocol') { should eq '2' }
|
16
|
+
end
|
17
|
+
"
|
18
|
+
|
19
|
+
def initialize(conf_path = nil, type = nil)
|
20
|
+
@conf_path = conf_path || '/etc/ssh/ssh_config'
|
21
|
+
typename = (@conf_path.include?('sshd') ? 'Server' : 'Client')
|
22
|
+
@type = type || "SSH #{typename} configuration #{conf_path}"
|
15
23
|
end
|
16
|
-
"
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@type = type || "SSH #{typename} configuration #{conf_path}"
|
22
|
-
end
|
25
|
+
def content
|
26
|
+
read_content
|
27
|
+
end
|
23
28
|
|
24
|
-
|
25
|
-
|
26
|
-
|
29
|
+
def params(*opts)
|
30
|
+
opts.inject(read_params) do |res, nxt|
|
31
|
+
res.respond_to?(:key) ? res[nxt] : nil
|
32
|
+
end
|
33
|
+
end
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
35
|
+
def method_missing(name)
|
36
|
+
param = read_params[name.to_s]
|
37
|
+
return nil if param.nil?
|
38
|
+
# extract first value if we have only one value in array
|
39
|
+
return param[0] if param.length == 1
|
40
|
+
param
|
31
41
|
end
|
32
|
-
end
|
33
42
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
# extract first value if we have only one value in array
|
38
|
-
return param[0] if param.length == 1
|
39
|
-
param
|
40
|
-
end
|
43
|
+
def to_s
|
44
|
+
'SSH Configuration'
|
45
|
+
end
|
41
46
|
|
42
|
-
|
43
|
-
'SSH Configuration'
|
44
|
-
end
|
47
|
+
private
|
45
48
|
|
46
|
-
|
49
|
+
def read_content
|
50
|
+
return @content if defined?(@content)
|
51
|
+
file = inspec.file(@conf_path)
|
52
|
+
if !file.file?
|
53
|
+
return skip_resource "Can't find file \"#{@conf_path}\""
|
54
|
+
end
|
47
55
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
return skip_resource "Can't find file \"#{@conf_path}\""
|
53
|
-
end
|
56
|
+
@content = file.content
|
57
|
+
if @content.empty? && file.size > 0
|
58
|
+
return skip_resource "Can't read file \"#{@conf_path}\""
|
59
|
+
end
|
54
60
|
|
55
|
-
|
56
|
-
if @content.empty? && file.size > 0
|
57
|
-
return skip_resource "Can't read file \"#{@conf_path}\""
|
61
|
+
@content
|
58
62
|
end
|
59
63
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
)
|
71
|
-
@params = conf.params
|
64
|
+
def read_params
|
65
|
+
return @params if defined?(@params)
|
66
|
+
return @params = {} if read_content.nil?
|
67
|
+
conf = SimpleConfig.new(
|
68
|
+
read_content,
|
69
|
+
assignment_re: /^\s*(\S+?)\s+(.*?)\s*$/,
|
70
|
+
multiple_values: true,
|
71
|
+
)
|
72
|
+
@params = conf.params
|
73
|
+
end
|
72
74
|
end
|
73
|
-
end
|
74
75
|
|
75
|
-
class SshdConf < SshConf
|
76
|
-
|
76
|
+
class SshdConf < SshConf
|
77
|
+
name 'sshd_config'
|
77
78
|
|
78
|
-
|
79
|
-
|
79
|
+
def initialize(path = nil)
|
80
|
+
super(path || '/etc/ssh/sshd_config')
|
81
|
+
end
|
80
82
|
end
|
81
83
|
end
|
data/lib/resources/user.rb
CHANGED
@@ -38,421 +38,423 @@
|
|
38
38
|
require 'utils/parser'
|
39
39
|
require 'utils/convert'
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
41
|
+
module Inspec::Resources
|
42
|
+
class User < Inspec.resource(1) # rubocop:disable Metrics/ClassLength
|
43
|
+
name 'user'
|
44
|
+
desc 'Use the user InSpec audit resource to test user profiles, including the groups to which they belong, the frequency of required password changes, the directory paths to home and shell.'
|
45
|
+
example "
|
46
|
+
describe user('root') do
|
47
|
+
it { should exist }
|
48
|
+
its('uid') { should eq 1234 }
|
49
|
+
its('gid') { should eq 1234 }
|
50
|
+
end
|
51
|
+
"
|
52
|
+
def initialize(user)
|
53
|
+
@user = user
|
54
|
+
|
55
|
+
# select package manager
|
56
|
+
@user_provider = nil
|
57
|
+
os = inspec.os
|
58
|
+
if os.linux?
|
59
|
+
@user_provider = LinuxUser.new(inspec)
|
60
|
+
elsif os.windows?
|
61
|
+
@user_provider = WindowsUser.new(inspec)
|
62
|
+
elsif ['darwin'].include?(os[:family])
|
63
|
+
@user_provider = DarwinUser.new(inspec)
|
64
|
+
elsif ['freebsd'].include?(os[:family])
|
65
|
+
@user_provider = FreeBSDUser.new(inspec)
|
66
|
+
elsif ['aix'].include?(os[:family])
|
67
|
+
@user_provider = AixUser.new(inspec)
|
68
|
+
elsif os.solaris?
|
69
|
+
@user_provider = SolarisUser.new(inspec)
|
70
|
+
else
|
71
|
+
return skip_resource 'The `user` resource is not supported on your OS yet.'
|
72
|
+
end
|
71
73
|
end
|
72
|
-
end
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
75
|
+
def exists?
|
76
|
+
!identity.nil? && !identity[:user].nil?
|
77
|
+
end
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
|
79
|
+
def uid
|
80
|
+
identity[:uid] unless identity.nil?
|
81
|
+
end
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
|
83
|
+
def gid
|
84
|
+
identity[:gid] unless identity.nil?
|
85
|
+
end
|
85
86
|
|
86
|
-
|
87
|
-
|
88
|
-
|
87
|
+
def group
|
88
|
+
identity[:group] unless identity.nil?
|
89
|
+
end
|
89
90
|
|
90
|
-
|
91
|
-
|
92
|
-
|
91
|
+
def groups
|
92
|
+
identity[:groups] unless identity.nil?
|
93
|
+
end
|
93
94
|
|
94
|
-
|
95
|
-
|
96
|
-
|
95
|
+
def home
|
96
|
+
meta_info[:home] unless meta_info.nil?
|
97
|
+
end
|
97
98
|
|
98
|
-
|
99
|
-
|
100
|
-
|
99
|
+
def shell
|
100
|
+
meta_info[:shell] unless meta_info.nil?
|
101
|
+
end
|
101
102
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
103
|
+
# returns the minimum days between password changes
|
104
|
+
def mindays
|
105
|
+
credentials[:mindays] unless credentials.nil?
|
106
|
+
end
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
108
|
+
# returns the maximum days between password changes
|
109
|
+
def maxdays
|
110
|
+
credentials[:maxdays] unless credentials.nil?
|
111
|
+
end
|
111
112
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
# returns the days for password change warning
|
114
|
+
def warndays
|
115
|
+
credentials[:warndays] unless credentials.nil?
|
116
|
+
end
|
116
117
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
118
|
+
# implement 'mindays' method to be compatible with serverspec
|
119
|
+
def minimum_days_between_password_change
|
120
|
+
deprecated('minimum_days_between_password_change', "Please use 'its(:mindays)'")
|
121
|
+
mindays
|
122
|
+
end
|
122
123
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
124
|
+
# implement 'maxdays' method to be compatible with serverspec
|
125
|
+
def maximum_days_between_password_change
|
126
|
+
deprecated('maximum_days_between_password_change', "Please use 'its(:maxdays)'")
|
127
|
+
maxdays
|
128
|
+
end
|
128
129
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
130
|
+
# implements rspec has matcher, to be compatible with serverspec
|
131
|
+
# @see: https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/has.rb
|
132
|
+
def has_uid?(compare_uid)
|
133
|
+
deprecated('has_uid?')
|
134
|
+
uid == compare_uid
|
135
|
+
end
|
135
136
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
137
|
+
def has_home_directory?(compare_home)
|
138
|
+
deprecated('has_home_directory?', "Please use 'its(:home)'")
|
139
|
+
home == compare_home
|
140
|
+
end
|
140
141
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
142
|
+
def has_login_shell?(compare_shell)
|
143
|
+
deprecated('has_login_shell?', "Please use 'its(:shell)'")
|
144
|
+
shell == compare_shell
|
145
|
+
end
|
145
146
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
147
|
+
def has_authorized_key?(_compare_key)
|
148
|
+
deprecated('has_authorized_key?')
|
149
|
+
fail NotImplementedError
|
150
|
+
end
|
150
151
|
|
151
|
-
|
152
|
-
|
153
|
-
|
152
|
+
def deprecated(name, alternative = nil)
|
153
|
+
warn "[DEPRECATION] #{name} is deprecated. #{alternative}"
|
154
|
+
end
|
154
155
|
|
155
|
-
|
156
|
-
|
157
|
-
|
156
|
+
def to_s
|
157
|
+
"User #{@user}"
|
158
|
+
end
|
158
159
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
160
|
+
def identity
|
161
|
+
return @id_cache if defined?(@id_cache)
|
162
|
+
@id_cache = @user_provider.identity(@user) if !@user_provider.nil?
|
163
|
+
end
|
163
164
|
|
164
|
-
|
165
|
+
private
|
165
166
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
167
|
+
def meta_info
|
168
|
+
return @meta_cache if defined?(@meta_cache)
|
169
|
+
@meta_cache = @user_provider.meta_info(@user) if !@user_provider.nil?
|
170
|
+
end
|
170
171
|
|
171
|
-
|
172
|
-
|
173
|
-
|
172
|
+
def credentials
|
173
|
+
return @cred_cache if defined?(@cred_cache)
|
174
|
+
@cred_cache = @user_provider.credentials(@user) if !@user_provider.nil?
|
175
|
+
end
|
174
176
|
end
|
175
|
-
end
|
176
177
|
|
177
|
-
class UserInfo
|
178
|
-
|
179
|
-
|
180
|
-
attr_reader :inspec
|
181
|
-
def initialize(inspec)
|
182
|
-
@inspec = inspec
|
183
|
-
end
|
178
|
+
class UserInfo
|
179
|
+
include Converter
|
184
180
|
|
185
|
-
|
186
|
-
|
187
|
-
|
181
|
+
attr_reader :inspec
|
182
|
+
def initialize(inspec)
|
183
|
+
@inspec = inspec
|
184
|
+
end
|
188
185
|
|
189
|
-
|
190
|
-
|
191
|
-
attr_reader :inspec, :id_cmd
|
192
|
-
def initialize(inspec)
|
193
|
-
@inspec = inspec
|
194
|
-
@id_cmd ||= 'id'
|
195
|
-
super
|
186
|
+
def credentials(_username)
|
187
|
+
end
|
196
188
|
end
|
197
189
|
|
198
|
-
#
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
).params
|
207
|
-
end
|
190
|
+
# implements generic unix id handling
|
191
|
+
class UnixUser < UserInfo
|
192
|
+
attr_reader :inspec, :id_cmd
|
193
|
+
def initialize(inspec)
|
194
|
+
@inspec = inspec
|
195
|
+
@id_cmd ||= 'id'
|
196
|
+
super
|
197
|
+
end
|
208
198
|
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
multiple_values: false,
|
220
|
-
).params
|
221
|
-
|
222
|
-
{
|
223
|
-
uid: convert_to_i(parse_value(params['uid']).keys[0]),
|
224
|
-
user: parse_value(params['uid']).values[0],
|
225
|
-
gid: convert_to_i(parse_value(params['gid']).keys[0]),
|
226
|
-
group: parse_value(params['gid']).values[0],
|
227
|
-
groups: parse_value(params['groups']).values,
|
228
|
-
}
|
229
|
-
end
|
199
|
+
# parse one id entry like '0(wheel)''
|
200
|
+
def parse_value(line)
|
201
|
+
SimpleConfig.new(
|
202
|
+
line,
|
203
|
+
line_separator: ',',
|
204
|
+
assignment_re: /^\s*([^\(]*?)\s*\(\s*(.*?)\)*$/,
|
205
|
+
group_re: nil,
|
206
|
+
multiple_values: false,
|
207
|
+
).params
|
208
|
+
end
|
230
209
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
210
|
+
# extracts the identity
|
211
|
+
def identity(username)
|
212
|
+
cmd = inspec.command("#{id_cmd} #{username}")
|
213
|
+
return nil if cmd.exit_status != 0
|
214
|
+
|
215
|
+
# parse words
|
216
|
+
params = SimpleConfig.new(
|
217
|
+
parse_id_entries(cmd.stdout.chomp),
|
218
|
+
assignment_re: /^\s*([^=]*?)\s*=\s*(.*?)\s*$/,
|
219
|
+
group_re: nil,
|
220
|
+
multiple_values: false,
|
221
|
+
).params
|
222
|
+
|
223
|
+
{
|
224
|
+
uid: convert_to_i(parse_value(params['uid']).keys[0]),
|
225
|
+
user: parse_value(params['uid']).values[0],
|
226
|
+
gid: convert_to_i(parse_value(params['gid']).keys[0]),
|
227
|
+
group: parse_value(params['gid']).values[0],
|
228
|
+
groups: parse_value(params['groups']).values,
|
229
|
+
}
|
237
230
|
end
|
238
|
-
data.push(raw) if !raw.nil?
|
239
|
-
data.join("\n")
|
240
|
-
end
|
241
|
-
end
|
242
231
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
home: passwd['home'],
|
254
|
-
shell: passwd['shell'],
|
255
|
-
}
|
232
|
+
# splits the results of id into seperate lines
|
233
|
+
def parse_id_entries(raw)
|
234
|
+
data = []
|
235
|
+
until (index = raw.index(/\)\s{1}/)).nil?
|
236
|
+
data.push(raw[0, index+1]) # inclue closing )
|
237
|
+
raw = raw[index+2, raw.length-index-2]
|
238
|
+
end
|
239
|
+
data.push(raw) if !raw.nil?
|
240
|
+
data.join("\n")
|
241
|
+
end
|
256
242
|
end
|
257
243
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
params = SimpleConfig.new(
|
263
|
-
cmd.stdout.chomp,
|
264
|
-
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
265
|
-
group_re: nil,
|
266
|
-
multiple_values: false,
|
267
|
-
).params
|
268
|
-
|
269
|
-
{
|
270
|
-
mindays: convert_to_i(params['Minimum number of days between password change']),
|
271
|
-
maxdays: convert_to_i(params['Maximum number of days between password change']),
|
272
|
-
warndays: convert_to_i(params['Number of days of warning before password expires']),
|
273
|
-
}
|
274
|
-
end
|
275
|
-
end
|
244
|
+
class LinuxUser < UnixUser
|
245
|
+
include PasswdParser
|
246
|
+
include CommentParser
|
276
247
|
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
248
|
+
def meta_info(username)
|
249
|
+
cmd = inspec.command("getent passwd #{username}")
|
250
|
+
return nil if cmd.exit_status != 0
|
251
|
+
# returns: root:x:0:0:root:/root:/bin/bash
|
252
|
+
passwd = parse_passwd_line(cmd.stdout.chomp)
|
253
|
+
{
|
254
|
+
home: passwd['home'],
|
255
|
+
shell: passwd['shell'],
|
256
|
+
}
|
257
|
+
end
|
283
258
|
|
284
|
-
|
285
|
-
|
259
|
+
def credentials(username)
|
260
|
+
cmd = inspec.command("chage -l #{username}")
|
261
|
+
return nil if cmd.exit_status != 0
|
262
|
+
|
263
|
+
params = SimpleConfig.new(
|
264
|
+
cmd.stdout.chomp,
|
265
|
+
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
266
|
+
group_re: nil,
|
267
|
+
multiple_values: false,
|
268
|
+
).params
|
269
|
+
|
270
|
+
{
|
271
|
+
mindays: convert_to_i(params['Minimum number of days between password change']),
|
272
|
+
maxdays: convert_to_i(params['Maximum number of days between password change']),
|
273
|
+
warndays: convert_to_i(params['Number of days of warning before password expires']),
|
274
|
+
}
|
275
|
+
end
|
286
276
|
end
|
287
|
-
end
|
288
277
|
|
289
|
-
class
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
# yet it can be somewhere in the supplementary list if someone added root
|
295
|
-
# to a groups list in /etc/group
|
296
|
-
# we rearrange to expected list if that is the case
|
297
|
-
if id[:groups].first != id[:group]
|
298
|
-
id[:groups].reject! { |i| i == id[:group] } if id[:groups].include?(id[:group])
|
299
|
-
id[:groups].unshift(id[:group])
|
278
|
+
class SolarisUser < LinuxUser
|
279
|
+
def initialize(inspec)
|
280
|
+
@inspec = inspec
|
281
|
+
@id_cmd ||= 'id -a'
|
282
|
+
super
|
300
283
|
end
|
301
284
|
|
302
|
-
|
285
|
+
def credentials(_username)
|
286
|
+
nil
|
287
|
+
end
|
303
288
|
end
|
304
289
|
|
305
|
-
|
306
|
-
|
307
|
-
|
290
|
+
class AixUser < UnixUser
|
291
|
+
def identity(username)
|
292
|
+
id = super(username)
|
293
|
+
return nil if id.nil?
|
294
|
+
# AIX 'id' command doesn't include the primary group in the supplementary
|
295
|
+
# yet it can be somewhere in the supplementary list if someone added root
|
296
|
+
# to a groups list in /etc/group
|
297
|
+
# we rearrange to expected list if that is the case
|
298
|
+
if id[:groups].first != id[:group]
|
299
|
+
id[:groups].reject! { |i| i == id[:group] } if id[:groups].include?(id[:group])
|
300
|
+
id[:groups].unshift(id[:group])
|
301
|
+
end
|
302
|
+
|
303
|
+
id
|
304
|
+
end
|
308
305
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
shell: user[2],
|
313
|
-
}
|
314
|
-
end
|
306
|
+
def meta_info(username)
|
307
|
+
lsuser = inspec.command("lsuser -C -a home shell #{username}")
|
308
|
+
return nil if lsuser.exit_status != 0
|
315
309
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
310
|
+
user = lsuser.stdout.chomp.split("\n").last.split(':')
|
311
|
+
{
|
312
|
+
home: user[1],
|
313
|
+
shell: user[2],
|
314
|
+
}
|
315
|
+
end
|
321
316
|
|
322
|
-
|
317
|
+
def credentials(username)
|
318
|
+
cmd = inspec.command(
|
319
|
+
"lssec -c -f /etc/security/user -s #{username} -a minage -a maxage -a pwdwarntime",
|
320
|
+
)
|
321
|
+
return nil if cmd.exit_status != 0
|
323
322
|
|
324
|
-
|
325
|
-
mindays: user_sec[1].to_i * 7,
|
326
|
-
maxdays: user_sec[2].to_i * 7,
|
327
|
-
warndays: user_sec[3].to_i,
|
328
|
-
}
|
329
|
-
end
|
330
|
-
end
|
323
|
+
user_sec = cmd.stdout.chomp.split("\n").last.split(':')
|
331
324
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
def meta_info(username)
|
339
|
-
cmd = inspec.command("dscl -q . -read /Users/#{username} NFSHomeDirectory PrimaryGroupID RecordName UniqueID UserShell")
|
340
|
-
return nil if cmd.exit_status != 0
|
341
|
-
|
342
|
-
params = SimpleConfig.new(
|
343
|
-
cmd.stdout.chomp,
|
344
|
-
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
345
|
-
group_re: nil,
|
346
|
-
multiple_values: false,
|
347
|
-
).params
|
348
|
-
|
349
|
-
{
|
350
|
-
home: params['NFSHomeDirectory'],
|
351
|
-
shell: params['UserShell'],
|
352
|
-
}
|
325
|
+
{
|
326
|
+
mindays: user_sec[1].to_i * 7,
|
327
|
+
maxdays: user_sec[2].to_i * 7,
|
328
|
+
warndays: user_sec[3].to_i,
|
329
|
+
}
|
330
|
+
end
|
353
331
|
end
|
354
|
-
end
|
355
332
|
|
356
|
-
#
|
357
|
-
# @see
|
358
|
-
#
|
359
|
-
#
|
360
|
-
# -
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
333
|
+
# we do not use 'finger' for MacOS, because it is harder to parse data with it
|
334
|
+
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/fingerd.8.html
|
335
|
+
# instead we use 'dscl' to request user data
|
336
|
+
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dscl.1.html
|
337
|
+
# @see http://superuser.com/questions/592921/mac-osx-users-vs-dscl-command-to-list-user
|
338
|
+
class DarwinUser < UnixUser
|
339
|
+
def meta_info(username)
|
340
|
+
cmd = inspec.command("dscl -q . -read /Users/#{username} NFSHomeDirectory PrimaryGroupID RecordName UniqueID UserShell")
|
341
|
+
return nil if cmd.exit_status != 0
|
342
|
+
|
343
|
+
params = SimpleConfig.new(
|
344
|
+
cmd.stdout.chomp,
|
345
|
+
assignment_re: /^\s*([^:]*?)\s*:\s*(.*?)\s*$/,
|
346
|
+
group_re: nil,
|
347
|
+
multiple_values: false,
|
348
|
+
).params
|
349
|
+
|
350
|
+
{
|
351
|
+
home: params['NFSHomeDirectory'],
|
352
|
+
shell: params['UserShell'],
|
353
|
+
}
|
354
|
+
end
|
376
355
|
end
|
377
|
-
end
|
378
356
|
|
379
|
-
#
|
380
|
-
# @see https://
|
381
|
-
# @see https://
|
382
|
-
#
|
383
|
-
#
|
384
|
-
#
|
385
|
-
#
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
357
|
+
# FreeBSD recommends to use the 'pw' command for user management
|
358
|
+
# @see: https://www.freebsd.org/doc/handbook/users-synopsis.html
|
359
|
+
# @see: https://www.freebsd.org/cgi/man.cgi?pw(8)
|
360
|
+
# It offers the following commands:
|
361
|
+
# - adduser(8) The recommended command-line application for adding new users.
|
362
|
+
# - rmuser(8) The recommended command-line application for removing users.
|
363
|
+
# - chpass(1) A flexible tool for changing user database information.
|
364
|
+
# - passwd(1) The command-line tool to change user passwords.
|
365
|
+
class FreeBSDUser < UnixUser
|
366
|
+
include PasswdParser
|
367
|
+
|
368
|
+
def meta_info(username)
|
369
|
+
cmd = inspec.command("pw usershow #{username} -7")
|
370
|
+
return nil if cmd.exit_status != 0
|
371
|
+
# returns: root:*:0:0:Charlie &:/root:/bin/csh
|
372
|
+
passwd = parse_passwd_line(cmd.stdout.chomp)
|
373
|
+
{
|
374
|
+
home: passwd['home'],
|
375
|
+
shell: passwd['shell'],
|
376
|
+
}
|
377
|
+
end
|
399
378
|
end
|
400
379
|
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
|
423
|
-
ConvertTo-Json
|
424
|
-
EOH
|
425
|
-
|
426
|
-
cmd = inspec.script(script)
|
427
|
-
|
428
|
-
# cannot rely on exit code for now, successful command returns exit code 1
|
429
|
-
# return nil if cmd.exit_status != 0, try to parse json
|
430
|
-
begin
|
431
|
-
params = JSON.parse(cmd.stdout)
|
432
|
-
rescue JSON::ParserError => _e
|
433
|
-
return nil
|
434
|
-
end
|
435
|
-
|
436
|
-
user = params['User']['Caption'] unless params['User'].nil?
|
437
|
-
groups = params['Groups']
|
438
|
-
# if groups is no array, generate one
|
439
|
-
groups = [groups] if !groups.is_a?(Array)
|
440
|
-
groups = groups.map { |grp| grp['Caption'] } unless params['Groups'].nil?
|
441
|
-
|
442
|
-
{
|
443
|
-
uid: nil,
|
444
|
-
user: user,
|
445
|
-
gid: nil,
|
446
|
-
group: nil,
|
447
|
-
groups: groups,
|
448
|
-
}
|
449
|
-
end
|
380
|
+
# For now, we stick with WMI Win32_UserAccount
|
381
|
+
# @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
|
382
|
+
# @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx
|
383
|
+
#
|
384
|
+
# using Get-AdUser would be the best command for domain machines, but it will not be installed
|
385
|
+
# on client machines by default
|
386
|
+
# @see https://technet.microsoft.com/en-us/library/ee617241.aspx
|
387
|
+
# @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx
|
388
|
+
# @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/
|
389
|
+
# @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet
|
390
|
+
#
|
391
|
+
# Just for reference, we could also use ADSI (Active Directory Service Interfaces)
|
392
|
+
# @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
|
393
|
+
class WindowsUser < UserInfo
|
394
|
+
# parse windows account name
|
395
|
+
def parse_windows_account(username)
|
396
|
+
account = username.split('\\')
|
397
|
+
name = account.pop
|
398
|
+
domain = account.pop if account.size > 0
|
399
|
+
[name, domain]
|
400
|
+
end
|
450
401
|
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
402
|
+
def identity(username)
|
403
|
+
# extract domain/user information
|
404
|
+
account, domain = parse_windows_account(username)
|
405
|
+
|
406
|
+
# TODO: escape content
|
407
|
+
if !domain.nil?
|
408
|
+
filter = "Name = '#{account}' and Domain = '#{domain}'"
|
409
|
+
else
|
410
|
+
filter = "Name = '#{account}' and LocalAccount = true"
|
411
|
+
end
|
412
|
+
|
413
|
+
script = <<-EOH
|
414
|
+
# find user
|
415
|
+
$user = Get-WmiObject Win32_UserAccount -filter "#{filter}"
|
416
|
+
# get related groups
|
417
|
+
$groups = $user.GetRelated('Win32_Group') | Select-Object -Property Caption, Domain, Name, LocalAccount, SID, SIDType, Status
|
418
|
+
# filter user information
|
419
|
+
$user = $user | Select-Object -Property Caption, Description, Domain, Name, LocalAccount, Lockout, PasswordChangeable, PasswordExpires, PasswordRequired, SID, SIDType, Status
|
420
|
+
# build response object
|
421
|
+
New-Object -Type PSObject | `
|
422
|
+
Add-Member -MemberType NoteProperty -Name User -Value ($user) -PassThru | `
|
423
|
+
Add-Member -MemberType NoteProperty -Name Groups -Value ($groups) -PassThru | `
|
424
|
+
ConvertTo-Json
|
425
|
+
EOH
|
426
|
+
|
427
|
+
cmd = inspec.script(script)
|
428
|
+
|
429
|
+
# cannot rely on exit code for now, successful command returns exit code 1
|
430
|
+
# return nil if cmd.exit_status != 0, try to parse json
|
431
|
+
begin
|
432
|
+
params = JSON.parse(cmd.stdout)
|
433
|
+
rescue JSON::ParserError => _e
|
434
|
+
return nil
|
435
|
+
end
|
436
|
+
|
437
|
+
user = params['User']['Caption'] unless params['User'].nil?
|
438
|
+
groups = params['Groups']
|
439
|
+
# if groups is no array, generate one
|
440
|
+
groups = [groups] if !groups.is_a?(Array)
|
441
|
+
groups = groups.map { |grp| grp['Caption'] } unless params['Groups'].nil?
|
442
|
+
|
443
|
+
{
|
444
|
+
uid: nil,
|
445
|
+
user: user,
|
446
|
+
gid: nil,
|
447
|
+
group: nil,
|
448
|
+
groups: groups,
|
449
|
+
}
|
450
|
+
end
|
451
|
+
|
452
|
+
# not implemented yet
|
453
|
+
def meta_info(_username)
|
454
|
+
{
|
455
|
+
home: nil,
|
456
|
+
shell: nil,
|
457
|
+
}
|
458
|
+
end
|
457
459
|
end
|
458
460
|
end
|