inspec/lib/resources/groups.rb
Miah Johnson 7a6119e7a6 Add missing tests for groups resource, document members property, and assorted fixes. (#3467)
* Add missing tests for groups resource, document members propery, and assorted fixes.

Update existing documentation for group resource.
Add documentation for groups resource.
Update group resource tests to test members property.
Change groups resource members property to have simple style. (this
ensures members is a single array)
remove deprecated have_gid propery.
change `if !` to `unless`
Remove early return from members method. This prevented members from
working correctly on any OS other than Windows.
Add missing tests for the groups resource.
remove tests for has_gid

Signed-off-by: Miah Johnson <miah@chia-pet.org>

* Fix comments

Signed-off-by: Jared Quick <jquick@chef.io>
2018-10-18 16:48:30 -04:00

220 lines
6.4 KiB
Ruby

# encoding: utf-8
require 'utils/filter'
module Inspec::Resources
# This file contains two resources, the `group` and `groups` resource.
# The `group` resource is optimized for requests that verify specific groups
# that you know upfront for testing. If you need to query all groups or search
# specific groups with certain properties, use the `groups` resource.
module GroupManagementSelector
# select group provider based on the operating system
# returns nil, if no group manager was found for the operating system
def select_group_manager(os)
@group_provider = if os.darwin?
DarwinGroup.new(inspec)
elsif os.unix?
UnixGroup.new(inspec)
elsif os.windows?
WindowsGroup.new(inspec)
end
end
end
class Groups < Inspec.resource(1)
include GroupManagementSelector
name 'groups'
supports platform: 'unix'
supports platform: 'windows'
desc 'Use the group InSpec audit resource to test groups on the system. Groups can be filtered.'
example "
describe groups.where { name == 'root'} do
its('names') { should eq ['root'] }
its('gids') { should eq [0] }
end
describe groups.where { name == 'Administrators'} do
its('names') { should eq ['Administrators'] }
its('gids') { should eq ['S-1-5-32-544'] }
end
"
def initialize
# select group manager
@group_provider = select_group_manager(inspec.os)
return skip_resource 'The `groups` resource is not supported on your OS yet.' if @group_provider.nil?
end
filter = FilterTable.create
filter.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
filter.register_column(:names, field: 'name')
.register_column(:gids, field: 'gid')
.register_column(:domains, field: 'domain')
.register_column(:members, field: 'members', style: :simple)
filter.install_filter_methods_on_resource(self, :collect_group_details)
def to_s
'Groups'
end
private
# collects information about every group
def collect_group_details
return @groups_cache ||= @group_provider.groups unless @group_provider.nil?
[]
end
end
# Usage:
# describe group('root') do
# it { should exist }
# its('gid') { should eq 0 }
# end
#
class Group < Inspec.resource(1)
include GroupManagementSelector
name 'group'
supports platform: 'unix'
supports platform: 'windows'
desc 'Use the group InSpec audit resource to test groups on the system.'
example "
describe group('root') do
it { should exist }
its('gid') { should eq 0 }
end
describe group('Administrators') do
its('members') { should include 'Administrator' }
end
"
def initialize(groupname)
@group = groupname
# select group manager
@group_provider = select_group_manager(inspec.os)
return skip_resource 'The `group` resource is not supported on your OS yet.' if @group_provider.nil?
end
# verifies if a group exists
def exists?
!group_info.entries.empty?
end
def gid
flatten_entry(group_info, 'gid')
end
def members
flatten_entry(group_info, 'members')
end
def local
# at this point the implementation only returns local groups
true
end
def to_s
"Group #{@group}"
end
private
def flatten_entry(group_info, prop)
entries = group_info.entries
if entries.empty?
nil
elsif entries.size == 1
entries.first.send(prop)
else
raise 'found more than one group with the same name, please use `groups` resource'
end
end
def group_info
# we need a local copy for the block
group = @group.dup
@groups_cache ||= inspec.groups.where { name == group }
end
end
class GroupInfo
attr_reader :inspec
def initialize(inspec)
@inspec = inspec
end
def groups
raise 'group provider must implement the `groups` method'
end
end
# implements generic unix groups via /etc/group
class UnixGroup < GroupInfo
def groups
inspec.etc_group.entries
end
end
# OSX uses opendirectory for groups, so `/etc/group` may not be fully accurate
# This uses `dscacheutil` to get the group info instead of `etc_group`
class DarwinGroup < GroupInfo
def groups
group_info = inspec.command('dscacheutil -q group').stdout.split("\n\n")
groups = []
regex = /^([^:]*?)\s*:\s(.*?)\s*$/
group_info.each do |data|
groups << inspec.parse_config(data, assignment_regex: regex).params
end
# Convert the `dscacheutil` groups to match `inspec.etc_group.entries`
groups.each { |g| g['gid'] = g['gid'].to_i }
groups.each do |g|
next if g['users'].nil?
g['members'] = g.delete('users')
g['members'].tr!(' ', ',')
end
end
end
class WindowsGroup < GroupInfo
# returns all local groups
def groups
script = <<-EOH
Function ConvertTo-SID { Param([byte[]]$BinarySID)
(New-Object System.Security.Principal.SecurityIdentifier($BinarySID,0)).Value
}
$Computername = $Env:Computername
$adsi = [ADSI]"WinNT://$Computername"
$groups = $adsi.Children | where {$_.SchemaClassName -eq 'group'} | ForEach {
$name = $_.Name[0]
$sid = ConvertTo-SID -BinarySID $_.ObjectSID[0]
$group =[ADSI]$_.Path
$members = $_.Members() | Foreach-Object { $_.GetType().InvokeMember('Name', 'GetProperty', $null, $_, $null) }
# An empty collection of these objects isn't properly converted to an empty array by ConvertTo-Json
if(-not [bool]$members) {
$members = @()
}
new-object psobject -property @{name = $group.Name[0]; gid = $sid; domain = $Computername; members = $members}
}
$groups | ConvertTo-Json -Depth 3
EOH
cmd = inspec.powershell(script)
# 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
groups = JSON.parse(cmd.stdout)
rescue JSON::ParserError => _e
return []
end
# ensure we have an array of groups
groups = [groups] unless groups.is_a?(Array)
groups
end
end
end