mirror of
https://github.com/inspec/inspec
synced 2024-11-14 00:47:10 +00:00
71ba5018d2
* implement members property * flatten groups entry, extract flatten helper * lints * more idiomatic spec, add example of members testing Signed-off-by: Tor Magnus Rakvåg <tm@intility.no>
232 lines
6.6 KiB
Ruby
232 lines
6.6 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.add_accessor(:where)
|
|
.add_accessor(:entries)
|
|
.add(:names, field: 'name')
|
|
.add(:gids, field: 'gid')
|
|
.add(:domains, field: 'domain')
|
|
.add(:members, field: 'members')
|
|
.add(:exists?) { |x| !x.entries.empty? }
|
|
filter.connect(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
|
|
#
|
|
# deprecated has matcher
|
|
# describe group('root') do
|
|
# it { should have_gid 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
|
|
|
|
# implements rspec has matcher, to be compatible with serverspec
|
|
def has_gid?(compare_gid)
|
|
gid == compare_gid
|
|
end
|
|
|
|
def members
|
|
return unless inspec.os.windows?
|
|
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] if !groups.is_a?(Array)
|
|
groups
|
|
end
|
|
end
|
|
end
|