2015-10-03 11:32:19 +00:00
# encoding: utf-8
# author: Christoph Hartmann
2015-10-06 16:55:44 +00:00
# author: Dominik Richter
2015-10-03 11:32:19 +00:00
# Usage:
#
# describe user('root') do
# it { should exist }
# its(:uid) { should eq 0 }
# its(:gid) { should eq 0 }
# its(:group) { should eq 'root' }
# its(:groups) { should eq ['root', 'wheel']}
# its(:home) { should eq '/root' }
# its(:shell) { should eq '/bin/bash' }
# its(:mindays) { should eq 0 }
# its(:maxdays) { should eq 99 }
# its(:warndays) { should eq 5 }
# end
#
# The following Serverspec matchers are deprecated in favor for direct value access
#
# describe user('root') do
# it { should belong_to_group 'root' }
# it { should have_uid 0 }
# it { should have_home_directory '/root' }
# it { should have_login_shell '/bin/bash' }
# its(:minimum_days_between_password_change) { should eq 0 }
# its(:maximum_days_between_password_change) { should eq 99 }
# end
# ServerSpec tests that are not supported:
#
# describe user('root') do
# it { should have_authorized_key 'ssh-rsa ADg54...3434 user@example.local' }
# its(:encrypted_password) { should eq 1234 }
# end
2015-10-04 16:07:45 +00:00
require 'utils/parser'
2015-10-05 09:40:34 +00:00
require 'utils/convert'
2015-10-04 16:07:45 +00:00
2016-01-28 13:26:31 +00:00
class User < Inspec . resource ( 1 ) # rubocop:disable Metrics/ClassLength
2015-10-03 11:32:19 +00:00
name 'user'
2015-11-27 13:02:38 +00:00
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.'
example "
describe user ( 'root' ) do
it { should exist }
its ( 'uid' ) { should eq 1234 }
its ( 'gid' ) { should eq 1234 }
end
"
2015-10-03 11:32:19 +00:00
def initialize ( user )
@user = user
# select package manager
@user_provider = nil
2016-01-28 13:24:51 +00:00
os = inspec . os
if os . linux?
2015-10-26 03:04:18 +00:00
@user_provider = LinuxUser . new ( inspec )
2016-01-28 13:24:51 +00:00
elsif os . windows?
2015-10-26 03:04:18 +00:00
@user_provider = WindowsUser . new ( inspec )
2016-01-28 13:24:51 +00:00
elsif [ 'darwin' ] . include? ( os [ :family ] )
2015-10-26 03:04:18 +00:00
@user_provider = DarwinUser . new ( inspec )
2016-01-28 13:24:51 +00:00
elsif [ 'freebsd' ] . include? ( os [ :family ] )
2015-10-26 03:04:18 +00:00
@user_provider = FreeBSDUser . new ( inspec )
2016-01-28 13:24:51 +00:00
elsif [ 'aix' ] . include? ( os [ :family ] )
2015-12-19 00:45:26 +00:00
@user_provider = AixUser . new ( inspec )
2016-01-28 13:26:31 +00:00
elsif os . solaris?
@user_provider = SolarisUser . new ( inspec )
2015-10-03 11:32:19 +00:00
else
2015-10-21 17:32:25 +00:00
return skip_resource 'The `user` resource is not supported on your OS yet.'
2015-10-03 11:32:19 +00:00
end
end
def exists?
2016-01-28 13:26:31 +00:00
! identity . nil? && ! identity [ :user ] . nil?
2015-10-03 11:32:19 +00:00
end
def uid
2016-01-28 13:26:31 +00:00
identity . nil? ? nil : identity [ :uid ]
2015-10-03 11:32:19 +00:00
end
def gid
2016-01-28 13:26:31 +00:00
identity . nil? ? nil : identity [ :gid ]
2015-10-03 11:32:19 +00:00
end
def group
2016-01-28 13:26:31 +00:00
identity . nil? ? nil : identity [ :group ]
2015-10-03 11:32:19 +00:00
end
def groups
2016-01-28 13:26:31 +00:00
identity . nil? ? nil : identity [ :groups ]
2015-10-03 11:32:19 +00:00
end
def home
2015-10-05 09:22:03 +00:00
meta_info . nil? ? nil : meta_info [ :home ]
2015-10-03 11:32:19 +00:00
end
def shell
2015-10-05 09:22:03 +00:00
meta_info . nil? ? nil : meta_info [ :shell ]
2015-10-03 11:32:19 +00:00
end
# returns the minimum days between password changes
def mindays
2015-10-05 09:40:34 +00:00
credentials . nil? ? nil : credentials [ :mindays ]
2015-10-03 11:32:19 +00:00
end
# returns the maximum days between password changes
def maxdays
2015-10-05 09:40:34 +00:00
credentials . nil? ? nil : credentials [ :maxdays ]
2015-10-03 11:32:19 +00:00
end
# returns the days for password change warning
def warndays
2015-10-05 09:40:34 +00:00
credentials . nil? ? nil : credentials [ :warndays ]
2015-10-03 11:32:19 +00:00
end
# implement 'mindays' method to be compatible with serverspec
def minimum_days_between_password_change
deprecated ( 'minimum_days_between_password_change' , " Please use 'its(:mindays)' " )
mindays
end
# implement 'maxdays' method to be compatible with serverspec
def maximum_days_between_password_change
deprecated ( 'maximum_days_between_password_change' , " Please use 'its(:maxdays)' " )
maxdays
end
# implements rspec has matcher, to be compatible with serverspec
# @see: https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/built_in/has.rb
def has_uid? ( compare_uid )
deprecated ( 'has_uid?' )
uid == compare_uid
end
def has_home_directory? ( compare_home )
deprecated ( 'has_home_directory?' , " Please use 'its(:home)' " )
home == compare_home
end
def has_login_shell? ( compare_shell )
deprecated ( 'has_login_shell?' , " Please use 'its(:shell)' " )
shell == compare_shell
end
def has_authorized_key? ( _compare_key )
deprecated ( 'has_authorized_key?' )
fail NotImplementedError
end
def deprecated ( name , alternative = nil )
warn " [DEPRECATION] #{ name } is deprecated. #{ alternative } "
end
def to_s
2015-10-12 11:01:58 +00:00
" User #{ @user } "
2015-10-03 11:32:19 +00:00
end
2015-10-05 09:22:03 +00:00
2016-01-28 13:26:31 +00:00
def identity
2015-10-07 23:02:59 +00:00
return @id_cache if defined? ( @id_cache )
2015-10-05 09:46:48 +00:00
@id_cache = @user_provider . identity ( @user ) if ! @user_provider . nil?
2015-10-05 09:22:03 +00:00
end
2016-01-28 13:26:31 +00:00
private
2015-10-05 09:22:03 +00:00
def meta_info
2015-10-07 23:02:59 +00:00
return @meta_cache if defined? ( @meta_cache )
2015-10-05 09:46:48 +00:00
@meta_cache = @user_provider . meta_info ( @user ) if ! @user_provider . nil?
2015-10-05 09:22:03 +00:00
end
2015-10-05 09:40:34 +00:00
def credentials
2015-10-07 23:02:59 +00:00
return @cred_cache if defined? ( @cred_cache )
2015-10-05 09:46:48 +00:00
@cred_cache = @user_provider . credentials ( @user ) if ! @user_provider . nil?
2015-10-05 09:40:34 +00:00
end
2015-10-03 11:32:19 +00:00
end
class UserInfo
2015-10-05 09:40:34 +00:00
include Converter
2015-10-26 03:04:18 +00:00
attr_reader :inspec
2016-01-28 13:48:44 +00:00
def initialize ( inspec )
2015-10-26 03:04:18 +00:00
@inspec = inspec
2015-10-03 11:32:19 +00:00
end
2015-10-05 09:40:34 +00:00
def credentials ( _username )
end
2015-10-03 11:32:19 +00:00
end
# implements generic unix id handling
class UnixUser < UserInfo
2016-01-28 13:26:31 +00:00
attr_reader :inspec , :id_cmd
2016-01-28 13:48:44 +00:00
def initialize ( inspec )
2016-01-28 13:26:31 +00:00
@inspec = inspec
@id_cmd || = 'id'
super
end
2015-10-03 11:32:19 +00:00
# parse one id entry like '0(wheel)''
def parse_value ( line )
SimpleConfig . new (
line ,
2015-10-03 12:24:13 +00:00
line_separator : ',' ,
2015-10-03 11:32:19 +00:00
assignment_re : / ^ \ s*([^ \ (]*?) \ s* \ ( \ s*(.*?) \ )*$ / ,
group_re : nil ,
multiple_values : false ,
) . params
end
# extracts the identity
def identity ( username )
2016-01-28 13:26:31 +00:00
cmd = inspec . command ( " #{ id_cmd } #{ username } " )
2015-10-03 11:32:19 +00:00
return nil if cmd . exit_status != 0
# parse words
params = SimpleConfig . new (
2015-11-24 17:39:32 +00:00
parse_id_entries ( cmd . stdout . chomp ) ,
2015-10-03 11:32:19 +00:00
assignment_re : / ^ \ s*([^=]*?) \ s*= \ s*(.*?) \ s*$ / ,
group_re : nil ,
multiple_values : false ,
) . params
{
uid : convert_to_i ( parse_value ( params [ 'uid' ] ) . keys [ 0 ] ) ,
user : parse_value ( params [ 'uid' ] ) . values [ 0 ] ,
gid : convert_to_i ( parse_value ( params [ 'gid' ] ) . keys [ 0 ] ) ,
group : parse_value ( params [ 'gid' ] ) . values [ 0 ] ,
groups : parse_value ( params [ 'groups' ] ) . values ,
}
end
2015-11-24 17:39:32 +00:00
# splits the results of id into seperate lines
def parse_id_entries ( raw )
data = [ ]
until ( index = raw . index ( / \ ) \ s{1} / ) ) . nil?
data . push ( raw [ 0 , index + 1 ] ) # inclue closing )
raw = raw [ index + 2 , raw . length - index - 2 ]
end
data . push ( raw ) if ! raw . nil?
data . join ( " \n " )
end
2015-10-03 11:32:19 +00:00
end
class LinuxUser < UnixUser
2015-12-31 00:01:11 +00:00
include PasswdParser
include CommentParser
2015-10-04 16:07:45 +00:00
2015-10-03 11:58:45 +00:00
def meta_info ( username )
2015-10-26 03:04:18 +00:00
cmd = inspec . command ( " getent passwd #{ username } " )
2015-10-04 16:07:45 +00:00
return nil if cmd . exit_status != 0
# returns: root:x:0:0:root:/root:/bin/bash
passwd = parse_passwd_line ( cmd . stdout . chomp )
2015-10-03 11:32:19 +00:00
{
2015-10-04 16:07:45 +00:00
home : passwd [ 'home' ] ,
shell : passwd [ 'shell' ] ,
2015-10-03 11:32:19 +00:00
}
end
2015-10-05 09:40:34 +00:00
def credentials ( username )
2015-10-26 03:04:18 +00:00
cmd = inspec . command ( " chage -l #{ username } " )
2015-10-05 09:40:34 +00:00
return nil if cmd . exit_status != 0
params = SimpleConfig . new (
cmd . stdout . chomp ,
assignment_re : / ^ \ s*([^:]*?) \ s*: \ s*(.*?) \ s*$ / ,
group_re : nil ,
multiple_values : false ,
) . params
{
mindays : convert_to_i ( params [ 'Minimum number of days between password change' ] ) ,
maxdays : convert_to_i ( params [ 'Maximum number of days between password change' ] ) ,
warndays : convert_to_i ( params [ 'Number of days of warning before password expires' ] ) ,
}
end
2015-10-03 11:32:19 +00:00
end
2016-01-28 13:26:31 +00:00
class SolarisUser < LinuxUser
2016-01-28 13:48:44 +00:00
def initialize ( inspec )
2016-01-28 13:26:31 +00:00
@inspec = inspec
@id_cmd || = 'id -a'
super
end
def credentials ( _username )
nil
end
end
2015-12-19 00:45:26 +00:00
class AixUser < UnixUser
def identity ( username )
id = super ( username )
return nil if id . nil?
# AIX 'id' command doesn't include the primary group in the supplementary
# yet it can be somewhere in the supplementary list if someone added root
# to a groups list in /etc/group
# we rearrange to expected list if that is the case
if id [ :groups ] . first != id [ :group ]
id [ :groups ] . reject! { | i | i == id [ :group ] } if id [ :groups ] . include? ( id [ :group ] )
id [ :groups ] . unshift ( id [ :group ] )
end
id
end
def meta_info ( username )
lsuser = inspec . command ( " lsuser -C -a home shell #{ username } " )
return nil if lsuser . exit_status != 0
user = lsuser . stdout . chomp . split ( " \n " ) . last . split ( ':' )
{
2015-12-21 15:29:55 +00:00
home : user [ 1 ] ,
2015-12-19 00:45:26 +00:00
shell : user [ 2 ] ,
}
end
def credentials ( username )
cmd = inspec . command (
2016-01-15 04:15:10 +00:00
" lssec -c -f /etc/security/user -s #{ username } -a minage -a maxage -a pwdwarntime " ,
2015-12-19 00:45:26 +00:00
)
return nil if cmd . exit_status != 0
user_sec = cmd . stdout . chomp . split ( " \n " ) . last . split ( ':' )
{
mindays : user_sec [ 1 ] . to_i * 7 ,
maxdays : user_sec [ 2 ] . to_i * 7 ,
warndays : user_sec [ 3 ] . to_i ,
}
end
end
2015-10-03 11:32:19 +00:00
# we do not use 'finger' for MacOS, because it is harder to parse data with it
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/fingerd.8.html
# instead we use 'dscl' to request user data
# @see https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man1/dscl.1.html
# @see http://superuser.com/questions/592921/mac-osx-users-vs-dscl-command-to-list-user
class DarwinUser < UnixUser
def meta_info ( username )
2015-10-26 03:04:18 +00:00
cmd = inspec . command ( " dscl -q . -read /Users/ #{ username } NFSHomeDirectory PrimaryGroupID RecordName UniqueID UserShell " )
2015-10-03 11:32:19 +00:00
return nil if cmd . exit_status != 0
params = SimpleConfig . new (
cmd . stdout . chomp ,
assignment_re : / ^ \ s*([^:]*?) \ s*: \ s*(.*?) \ s*$ / ,
group_re : nil ,
multiple_values : false ,
) . params
{
home : params [ 'NFSHomeDirectory' ] ,
shell : params [ 'UserShell' ] ,
}
end
end
2015-10-03 11:58:45 +00:00
# FreeBSD recommends to use the 'pw' command for user management
# @see: https://www.freebsd.org/doc/handbook/users-synopsis.html
# @see: https://www.freebsd.org/cgi/man.cgi?pw(8)
# It offers the following commands:
# - adduser(8) The recommended command-line application for adding new users.
# - rmuser(8) The recommended command-line application for removing users.
# - chpass(1) A flexible tool for changing user database information.
# - passwd(1) The command-line tool to change user passwords.
class FreeBSDUser < UnixUser
2015-12-31 00:01:11 +00:00
include PasswdParser
2015-10-04 16:07:45 +00:00
def meta_info ( username )
2015-10-26 03:04:18 +00:00
cmd = inspec . command ( " pw usershow #{ username } -7 " )
2015-10-04 16:07:45 +00:00
return nil if cmd . exit_status != 0
# returns: root:*:0:0:Charlie &:/root:/bin/csh
passwd = parse_passwd_line ( cmd . stdout . chomp )
2015-10-04 15:20:59 +00:00
{
2015-10-04 16:07:45 +00:00
home : passwd [ 'home' ] ,
shell : passwd [ 'shell' ] ,
2015-10-04 15:20:59 +00:00
}
end
end
# For now, we stick with WMI Win32_UserAccount
# @see https://msdn.microsoft.com/en-us/library/aa394507(v=vs.85).aspx
# @see https://msdn.microsoft.com/en-us/library/aa394153(v=vs.85).aspx
#
# using Get-AdUser would be the best command for domain machines, but it will not be installed
# on client machines by default
# @see https://technet.microsoft.com/en-us/library/ee617241.aspx
# @see https://technet.microsoft.com/en-us/library/hh509016(v=WS.10).aspx
# @see http://woshub.com/get-aduser-getting-active-directory-users-data-via-powershell/
# @see http://stackoverflow.com/questions/17548523/the-term-get-aduser-is-not-recognized-as-the-name-of-a-cmdlet
#
# Just for reference, we could also use ADSI (Active Directory Service Interfaces)
# @see https://mcpmag.com/articles/2015/04/15/reporting-on-local-accounts.aspx
class WindowsUser < UserInfo
# parse windows account name
def parse_windows_account ( username )
account = username . split ( '\\' )
name = account . pop
domain = account . pop if account . size > 0
[ name , domain ]
end
def identity ( username )
# extract domain/user information
2015-10-05 12:57:09 +00:00
account , domain = parse_windows_account ( username )
2015-10-04 15:20:59 +00:00
# TODO: escape content
if ! domain . nil?
2015-10-05 12:57:09 +00:00
filter = " Name = ' #{ account } ' and Domain = ' #{ domain } ' "
2015-10-04 15:20:59 +00:00
else
2015-10-05 12:57:09 +00:00
filter = " Name = ' #{ account } ' and LocalAccount = true "
2015-10-04 15:20:59 +00:00
end
script = <<-EOH
# find user
$user = Get - WmiObject Win32_UserAccount - filter " #{ filter } "
# get related groups
$groups = $user . GetRelated ( 'Win32_Group' ) | Select - Object - Property Caption , Domain , Name , LocalAccount , SID , SIDType , Status
# filter user information
$user = $user | Select - Object - Property Caption , Description , Domain , Name , LocalAccount , Lockout , PasswordChangeable , PasswordExpires , PasswordRequired , SID , SIDType , Status
# build response object
New - Object - Type PSObject | `
Add - Member - MemberType NoteProperty - Name User - Value ( $user ) - PassThru | `
Add - Member - MemberType NoteProperty - Name Groups - Value ( $groups ) - PassThru | `
ConvertTo - Json
EOH
2015-10-26 03:04:18 +00:00
cmd = inspec . script ( script )
2015-10-04 15:20:59 +00:00
# cannot rely on exit code for now, successful command returns exit code 1
# return nil if cmd.exit_status != 0, try to parse json
begin
params = JSON . parse ( cmd . stdout )
rescue JSON :: ParserError = > _e
2015-10-05 09:22:03 +00:00
return nil
2015-10-04 15:20:59 +00:00
end
user = params [ 'User' ] [ 'Caption' ] unless params [ 'User' ] . nil?
2015-10-05 12:55:49 +00:00
groups = params [ 'Groups' ]
# if groups is no array, generate one
groups = [ groups ] if ! groups . is_a? ( Array )
groups = groups . map { | grp | grp [ 'Caption' ] } unless params [ 'Groups' ] . nil?
2015-10-04 15:20:59 +00:00
{
uid : nil ,
user : user ,
gid : nil ,
group : nil ,
groups : groups ,
}
end
2015-10-05 09:22:03 +00:00
# not implemented yet
2015-10-04 15:20:59 +00:00
def meta_info ( _username )
2015-10-03 11:58:45 +00:00
{
home : nil ,
shell : nil ,
}
end
end