Merge pull request #333 from foobarbam/master

Adding AIX classes, tests, and targetted ssh testing
This commit is contained in:
Christoph Hartmann 2016-01-14 23:20:14 -05:00
commit b278e4726c
21 changed files with 409 additions and 80 deletions

1
.gitignore vendored
View file

@ -8,3 +8,4 @@ Berksfile.lock
.bundle
.librarian
Puppetfile.lock
.kitchen.local.yml

View file

@ -46,6 +46,20 @@ namespace :test do
path = File.join(File.dirname(__FILE__), 'test', 'integration')
sh('sh', '-c', "cd #{path} && bundle exec kitchen test -c #{concurrency} -t .")
end
task :ssh, [:target] do |_t, args|
tests_path = File.join(File.dirname(__FILE__), 'test', 'integration', 'test', 'integration', 'default')
key_files = ENV['key_files'] || File.join(ENV['HOME'], '.ssh', 'id_rsa')
sh_cmd = "bin/inspec exec #{tests_path}/"
sh_cmd += ENV['test'] ? "#{ENV['test']}_spec.rb" : '*'
sh_cmd += " --sudo" unless args[:target].split('@')[0] == 'root'
sh_cmd += " -t ssh://#{args[:target]}"
sh_cmd += " --key_files=#{key_files}"
sh_cmd += " --format=#{ENV['format']}" if ENV['format']
sh('sh', '-c', sh_cmd)
end
end
# Print the current version of this gem or update it.

View file

@ -45,7 +45,7 @@ class EtcGroup < Inspec.resource(1)
# skip resource if it is not supported on current OS
return skip_resource 'The `etc_group` resource is not supported on your OS.' \
unless %w{ubuntu debian redhat fedora centos arch darwin freebsd wrlinux}.include?(inspec.os[:family])
unless %w{ubuntu debian redhat fedora centos arch darwin freebsd wrlinux aix}.include?(inspec.os[:family])
end
def groups(filter = nil)

View file

@ -92,9 +92,8 @@ module Inspec::Resources
def file_permission_granted?(flag, by_usergroup, by_specific_user)
fail 'Checking file permissions is not supported on your os' unless unix?
usergroup = usergroup_for(by_usergroup, by_specific_user)
if by_specific_user.nil?
if by_specific_user.nil? || by_specific_user.empty?
usergroup = usergroup_for(by_usergroup, by_specific_user)
check_file_permission_by_mask(usergroup, flag)
else
check_file_permission_by_user(by_specific_user, flag)
@ -113,6 +112,8 @@ module Inspec::Resources
perm_cmd = "su -s /bin/sh -c \"test -#{flag} #{path}\" #{user}"
elsif family == 'freebsd'
perm_cmd = "sudo -u #{user} test -#{flag} #{path}"
elsif family == 'aix'
perm_cmd = "su #{user} -c test -#{flag} #{path}"
else
return skip_resource 'The `file` resource does not support `by_user` on your OS.'
end

View file

@ -36,6 +36,8 @@ class Package < Inspec.resource(1)
@pkgman = Brew.new(inspec)
when 'windows'
@pkgman = WindowsPkg.new(inspec)
when 'aix'
@pkgman = BffPkg.new(inspec)
else
return skip_resource 'The `package` resource is not supported on your OS yet.'
end
@ -186,3 +188,19 @@ class WindowsPkg < PkgManagement
}
end
end
# AIX
class BffPkg < PkgManagement
def info(package_name)
cmd = inspec.command("lslpp -cL #{package_name}")
return nil if cmd.exit_status.to_i != 0
bff_pkg = cmd.stdout.split("\n").last.split(':')
{
name: bff_pkg[1],
installed: true,
version: bff_pkg[2],
type: 'bff',
}
end
end

View file

@ -34,8 +34,11 @@ class Port < Inspec.resource(1)
case inspec.os[:family]
when 'ubuntu', 'debian', 'redhat', 'fedora', 'centos', 'arch', 'wrlinux'
@port_manager = LinuxPorts.new(inspec)
when 'darwin'
@port_manager = DarwinPorts.new(inspec)
when 'darwin', 'aix'
# AIX: see http://www.ibm.com/developerworks/aix/library/au-lsof.html#resources
# and https://www-01.ibm.com/marketing/iwm/iwm/web/reg/pick.do?source=aixbp
# Darwin: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/lsof.8.html
@port_manager = LsofPorts.new(inspec)
when 'windows'
@port_manager = WindowsPorts.new(inspec)
when 'freebsd'
@ -129,44 +132,110 @@ class WindowsPorts < PortsInfo
end
end
# extracts udp and tcp ports from macos
class DarwinPorts < PortsInfo
# extracts udp and tcp ports from the lsof command
class LsofPorts < PortsInfo
attr_reader :lsof
def initialize(inspec, lsofpath = nil)
@lsof = lsofpath || 'lsof'
super(inspec)
end
def info
# collects UDP and TCP information
cmd = inspec.command('lsof -nP -iTCP -iUDP -sTCP:LISTEN')
return nil if cmd.exit_status.to_i != 0
ports = []
# split on each newline
cmd.stdout.each_line do |line|
# parse each line
# 1 - COMMAND, 2 - PID, 3 - USER, 4 - FD, 5 - TYPE, 6 - DEVICE, 7 - SIZE/OFF, 8 - NODE, 9 - NAME
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*$/.match(line)
# extract network info
net_addr = parsed[9].split(':')
# convert to number if possible
net_port = net_addr[1]
net_port = net_port.to_i if net_port =~ /^\d+$/
protocol = parsed[8].downcase
# add version to protocol
type = parsed[5].downcase
protocol += '6' if type == 'IPv6'
# check that lsof is available, otherwise fail
fail 'Please ensure `lsof` is available on the machine.' if !inspec.command(@lsof.to_s).exist?
# map data
port_info = {
port: net_port,
address: net_addr[0],
protocol: protocol,
process: parsed[1],
pid: parsed[2].to_i,
}
# -F p=pid, c=command, P=protocol name, t=type, n=internet addresses
# see 'OUTPUT FOR OTHER PROGRAMS' in LSOF(8)
lsof_cmd = inspec.command("#{@lsof} -nP -i -FpctPn")
return nil if lsof_cmd.exit_status.to_i != 0
# push data, if not headerfile
ports.push(port_info) if %w{tcp tcp6 udp udp6}.include?(protocol)
# map to desired return struct
lsof_parser(lsof_cmd).each do |process, port_ids|
pid, cmd = process.split(':')
port_ids.each do |port_str|
# should not break on ipv6 addresses
ipv, proto, port, host = port_str.split(':', 4)
ports.push({ port: port.to_i,
address: host,
protocol: ipv == 'ipv6' ? proto + '6' : proto,
process: cmd,
pid: pid.to_i })
end
end
ports
end
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
def lsof_parser(lsof_cmd)
procs = {}
# build this with formatted output (-F) from lsof
# procs = {
# '123:sshd' => [
# 'ipv4:tcp:22:127.0.0.1',
# 'ipv6:tcp:22:::1',
# 'ipv4:tcp:*',
# 'ipv6:tcp:*',
# ],
# '456:ntpd' => [
# 'ipv4:udp:123:*',
# 'ipv6:udp:123:*',
# ]
# }
proc_id = port_id = nil
lsof_cmd.stdout.each_line do |line|
line.chomp!
key = line.slice!(0)
case key
when 'p'
proc_id = line
port_id = nil
when 'c'
proc_id += ':' + line
when 't'
port_id = line.downcase
when 'P'
port_id += ':' + line.downcase
when 'n'
src, dst = line.split('->')
# skip active comm streams
next if dst
host, port = /^(\S+):(\d+|\*)$/.match(src)[1, 2]
# skip channels from port 0 - what does this mean?
next if port == '*'
# create new array stub if !exist?
procs[proc_id] = [] unless procs.key?(proc_id)
# change address '*' to zero
host = (port_id =~ /^ipv6:/) ? '[::]' : '0.0.0.0' if host == '*'
# entrust URI to scrub the host and port
begin
uri = URI("addr://#{host}:#{port}")
uri.host && uri.port
rescue => e
warn "could not parse URI 'addr://#{host}:#{port}' - #{e}"
next
end
# e.g. 'ipv4:tcp:22:127.0.0.1'
# strip ipv6 squares for inspec
port_id += ':' + port + ':' + host.gsub(/^\[|\]$/, '')
# lsof will give us another port unless it's done
procs[proc_id] << port_id
end
end
procs
end
end
# extract port information from netstat

View file

@ -80,6 +80,8 @@ class Service < Inspec.resource(1)
@service_mgmt = BSDInit.new(inspec)
when 'arch', 'opensuse'
@service_mgmt = Systemd.new(inspec)
when 'aix'
@service_mgmt = SrcMstr.new(inspec)
end
return skip_resource 'The `service` resource is not supported on your OS yet.' if @service_mgmt.nil?
@ -155,6 +157,53 @@ class Systemd < ServiceManager
end
end
# AIX services
class SrcMstr < ServiceManager
attr_reader :name
def info(service_name)
@name = service_name
running = status?
return nil if running.nil?
{
name: service_name,
description: nil,
installed: true,
running: running,
enabled: enabled?,
type: 'srcmstr',
}
end
def status?
status_cmd = inspec.command("lssrc -s #{@name}")
return nil if status_cmd.exit_status.to_i != 0
status_cmd.stdout.split(/\n/).last.chomp =~ /active$/ ? true : false
end
def enabled?
enabled_rc_tcpip? || enabled_inittab?
end
private
# #rubocop:disable Style/TrailingComma
def enabled_rc_tcpip?
if inspec.command(
"grep -v ^# /etc/rc.tcpip | grep 'start ' | grep -Eq '(/{0,1}| )#{@name} '",
).exit_status == 0
true
else
false
end
end
def enabled_inittab?
inspec.command("lsitab #{@name}").exit_status.to_i == 0 ? true : false
end
end
# @see: http://upstart.ubuntu.com
class Upstart < ServiceManager
def info(service_name)

View file

@ -62,6 +62,8 @@ class User < Inspec.resource(1)
@user_provider = DarwinUser.new(inspec)
when 'freebsd'
@user_provider = FreeBSDUser.new(inspec)
when 'aix'
@user_provider = AixUser.new(inspec)
else
return skip_resource 'The `user` resource is not supported on your OS yet.'
end
@ -263,6 +265,49 @@ class LinuxUser < UnixUser
end
end
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(':')
{
home: user[1],
shell: user[2],
}
end
def credentials(username)
cmd = inspec.command(
"lssec -c -f /etc/security/user -s #{username} -a minage -a maxage -a pwdwarntime",
)
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
# 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

View file

@ -137,8 +137,8 @@ class MockLoader
'/sbin/sysctl -q -n net.ipv4.conf.all.forwarding' => cmd.call('sbin_sysctl'),
# ports on windows
'Get-NetTCPConnection | Select-Object -Property State, Caption, Description, LocalAddress, LocalPort, RemoteAddress, RemotePort, DisplayName, Status | ConvertTo-Json' => cmd.call('get-net-tcpconnection'),
# ports on mac
'lsof -nP -iTCP -iUDP -sTCP:LISTEN' => cmd.call('lsof-np-itcp'),
# lsof formatted list of ports (should be quite cross platform)
'lsof -nP -i -FpctPn' => cmd.call('lsof-nP-i-FpctPn'),
# ports on linux
'netstat -tulpen' => cmd.call('netstat-tulpen'),
# ports on freebsd

View file

@ -6,8 +6,15 @@
if node['platform_family'] != 'windows'
gid = 'root'
gid = 'wheel' if node['platform_family'] == 'freebsd'
gid = case node['platform_family']
when 'aix'
'system'
when 'freebsd'
'wheel'
else
'root'
end
file '/tmp/file' do
mode '0765'

View file

@ -4,8 +4,14 @@
#
# adds a yaml file
gid = 'root'
gid = 'wheel' if node['platform_family'] == 'freebsd'
gid = case node['platform_family']
when 'aix'
'system'
when 'freebsd'
'wheel'
else
'root'
end
['yml', 'json', 'csv', 'ini'].each { |filetype|

View file

@ -1,13 +0,0 @@
# encoding: utf-8
if os.unix?
describe etc_group do
its('gids') { should_not contain_duplicates }
its('groups') { should include 'root' }
its('users') { should include 'root' }
end
describe etc_group.where(name: 'root') do
its('users') { should include 'root' }
end
end

View file

@ -0,0 +1,22 @@
# encoding: utf-8
root_group = case os[:family]
when 'aix'
'system'
when 'freebsd'
'wheel'
else
'root'
end
if os.unix?
describe etc_group do
its('gids') { should_not contain_duplicates }
its('groups') { should include root_group }
its('users') { should include 'root' }
end
describe etc_group.where(name: root_group) do
its('users') { should include 'root' }
end
end

View file

@ -1,6 +1,7 @@
# encoding: utf-8
if os[:family] == 'freebsd'
case os[:family]
when 'freebsd'
filedata = {
user: 'root',
group: 'wheel',
@ -8,6 +9,14 @@ if os[:family] == 'freebsd'
dir_md5sum: '598f4fe64aefab8f00bcbea4c9239abf',
dir_sha256sum: '9b4fb24edd6d1d8830e272398263cdbf026b97392cc35387b991dc0248a628f9',
}
when 'aix'
filedata = {
user: 'root',
group: 'system',
dir_content: nil,
dir_md5sum: nil,
dir_sha256sum: nil,
}
else
filedata = {
user: 'root',

View file

@ -30,3 +30,20 @@ if os[:family] == 'freebsd'
its('gid') { should eq nil }
end
end
if os[:family] == 'aix'
describe group('system') do
it { should exist }
its('gid') { should eq 0 }
end
describe group('bin') do
it { should exist }
its('gid') { should eq 2 }
end
describe group('noroot') do
it { should_not exist }
its('gid') { should eq nil }
end
end

View file

@ -1,11 +1,17 @@
# encoding: utf-8
if ['centos', 'fedora', 'opensuse', 'debian', 'ubuntu'].include?(os[:family])
case os[:family]
when 'centos', 'fedora', 'opensuse', 'debian', 'ubuntu'
describe package('curl') do
it { should be_installed }
end
describe package('nginx') do
it { should_not be_installed }
when 'aix'
describe package('bos.rte') do
it { should be_installed }
its('version') { should match /^(6|7)[.|\d]+\d$/ }
end
end
describe package('nginx') do
it { should_not be_installed }
end

View file

@ -5,5 +5,11 @@ if os.unix?
describe port(22) do
it { should be_listening }
its('protocols') { should include('tcp') }
its('protocols') { should_not include('udp') }
its('processes') { should include 'sshd' }
end
describe port(65432) do
it { should_not be_listening }
end
end

View file

@ -17,6 +17,9 @@ elsif ['windows'].include?(os[:family])
# Ubuntu
unavailable_service = 'sshd'
available_service = 'dhcp'
elsif ['aix'].include?(os[:family])
unavailable_service = 'clamav'
available_service = 'xntpd'
end
describe service(unavailable_service) do

View file

@ -1,8 +1,7 @@
# encoding: utf-8
# root test
if ['centos', 'fedora', 'opensuse', 'debian', 'ubuntu'].include?(os[:family])
case os[:family]
when 'centos', 'redhat', 'fedora', 'opensuse', 'debian', 'ubuntu'
userinfo = {
name: 'root',
group: 'root',
@ -14,10 +13,10 @@ if ['centos', 'fedora', 'opensuse', 'debian', 'ubuntu'].include?(os[:family])
}
# different groupset for centos 5
userinfo[:groups] = ["root", "bin", "daemon", "sys", "adm", "disk", "wheel"] if os[:release].to_i == 5
elsif ['freebsd'].include?(os[:family])
userinfo[:groups] = ["root", "bin", "daemon", "sys", "adm", "disk", "wheel"] \
if os[:release].to_i == 5
when 'freebsd'
userinfo = {
name: 'root',
group: 'wheel',
@ -28,8 +27,7 @@ elsif ['freebsd'].include?(os[:family])
shell: '/bin/csh',
}
elsif ['windows'].include?(os[:family])
when 'windows'
userinfo = {
name: 'Administrator',
group: nil,
@ -40,23 +38,35 @@ elsif ['windows'].include?(os[:family])
shell: nil,
}
when 'aix'
userinfo = {
name: 'bin',
group: 'bin',
uid: 2,
gid: 2,
groups: %w{bin sys adm},
home: '/bin',
shell: nil,
#mindays: 0,
#maxdays: 0,
warndays: 0,
}
else
userinfo = {}
end
if !os.windows?
case os[:family]
when 'windows'
describe user(userinfo[:name]) do
it { should exist }
it { should belong_to_group userinfo[:group] }
its('uid') { should eq userinfo[:uid] }
its('gid') { should eq userinfo[:gid] }
its('group') { should eq userinfo[:group] }
its('groups') { should eq userinfo[:groups] }
its('home') { should eq userinfo[:home] }
its('shell') { should eq userinfo[:shell] }
end
else
describe user(userinfo[:name]) do
it { should exist }
userinfo.each do |k, v|
next if k.to_sym == :name
its(k) { should eq v }
end
end
end

View file

@ -0,0 +1,63 @@
p697
cUserEvent
tIPv4
PUDP
n*:*
p6835
cVBoxHeadl
tIPv4
PTCP
n127.0.0.1:2022
tIPv4
PUDP
n*:61197
p2022
cjava
tIPv6
PTCP
n192.168.1.66:51098->172.18.1.63:22
p8708
csshd
tIPv4
PTCP
n*:22
tIPv6
PTCP
n*:22
p13451
cssh
tIPv4
PTCP
n192.168.1.66:46951->192.168.1.80:22
p24137
cntpd
tIPv4
PUDP
n*:123
tIPv6
PUDP
n*:123
tIPv4
PUDP
n127.0.0.1:123
tIPv4
PUDP
n192.168.1.66:123
tIPv4
PUDP
n172.17.42.1:123
tIPv6
PUDP
n[2602:30a:c0d0:e070:8e70:5aff:fe66:6ebc]:123
tIPv6
PUDP
n[::1]:123
tIPv6
PUDP
n[fe80::8e70:5aff:fe66:6ebc]:123
tIPv6
PUDP
n[2602:30a:c0d0:e070:b1e3:fa7c:c1cd:e1dd]:123
tIPv4
PUDP
n9.48.126.170:123

View file

@ -1,4 +0,0 @@
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
UserEvent 697 chartmann 4u IPv4 0xf0f511b94b566c77 0t0 UDP *:*
VBoxHeadl 6835 chartmann 21u IPv4 0xf0f511b96bd8db37 0t0 TCP 127.0.0.1:2022 (LISTEN)
VBoxHeadl 6835 chartmann 23u IPv4 0xf0f511b94cc8c5d7 0t0 UDP *:61197