Uses netstat to detect open ports on AIX (#2210)

* Uses netstat to detect open ports on AIX

Signed-off-by: Keith Walters <keith.walters@cattywamp.us>

* Adds unit tests for AIX port resource

Signed-off-by: Keith Walters <keith.walters@cattywamp.us>
This commit is contained in:
Keith Walters 2017-10-10 04:54:18 -04:00 committed by Dominik Richter
parent 54136ac408
commit 2a8d6e0e91
6 changed files with 141 additions and 1 deletions

View file

@ -59,9 +59,11 @@ module Inspec::Resources
os = inspec.os
if os.linux?
LinuxPorts.new(inspec)
elsif %w{darwin aix}.include?(os[:family])
elsif os.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
AixPorts.new(inspec)
elsif os.darwin?
# Darwin: https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man8/lsof.8.html
LsofPorts.new(inspec)
elsif os.windows?
@ -263,6 +265,121 @@ module Inspec::Resources
end
end
class AixPorts < PortsInfo
def info
ports_via_netstat || ports_via_lsof
end
def ports_via_lsof
return nil unless inspec.command('lsof').exist?
LsofPorts.new(inspec).info
end
def ports_via_netstat
return nil unless inspec.command('netstat').exist?
cmd = inspec.command('netstat -Aan | grep LISTEN')
return nil unless cmd.exit_status.to_i.zero?
ports = []
# parse all lines
cmd.stdout.each_line do |line|
port_info = parse_netstat_line(line)
# only push protocols we are interested in
next unless %w{tcp tcp6 udp udp6}.include?(port_info['protocol'])
ports.push(port_info)
end
ports
end
def parse_netstat_line(line)
# parse each line
# 1 - Socket, 2 - Proto, 3 - Receive-Q, 4 - Send-Q, 5 - Local address, 6 - Foreign Address, 7 - State
parsed = /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)?\s+(\S+)/.match(line)
return {} if parsed.nil?
# parse ip4 and ip6 addresses
protocol = parsed[2].downcase
# detect protocol if not provided
protocol += '6' if parsed[5].count(':') > 1 && %w{tcp udp}.include?(protocol)
protocol.chop! if %w{tcp4 upd4}.include?(protocol)
# extract host and port information
host, port = parse_net_address(parsed[5], protocol)
return {} if host.nil?
# extract PID
cmd = inspec.command("rmsock #{parsed[1]} tcpcb")
parsed_pid = /^The socket (\S+) is being held by proccess (\d+) \((\S+)\)/.match(cmd.stdout)
return {} if parsed_pid.nil?
process = parsed_pid[3]
pid = parsed_pid[2]
pid = pid.to_i if pid =~ /^\d+$/
{
'port' => port,
'address' => host,
'protocol' => protocol,
'process' => process,
'pid' => pid,
}
end
def parse_net_address(net_addr, protocol)
# local/foreign addresses on AIX use a '.' to separate the addresss
# from the port
address, _sep, port = net_addr.rpartition('.')
if protocol.eql?('tcp6') || protocol.eql?('udp6')
ip6addr = address
# AIX uses the wildcard character for ipv6 addresses listening on
# all interfaces.
ip6addr = '::' if ip6addr =~ /^\*$/
# v6 addresses need to end in a double-colon when using
# shorthand notation. netstat ends with a single colon.
# IPAddr will fail to properly parse an address unless it
# uses a double-colon for short-hand notation.
ip6addr += ':' if ip6addr =~ /\w:$/
begin
ip_parser = IPAddr.new(ip6addr)
rescue IPAddr::InvalidAddressError
# This IP is not parsable. There appears to be a bug in netstat
# output that truncates link-local IP addresses:
# example: udp6 0 0 fe80::42:acff:fe11::123 :::* 0 54550 3335/ntpd
# actual link address: inet6 fe80::42:acff:fe11:5/64 scope link
#
# in this example, the "5" is truncated making the netstat output
# an invalid IP address.
return [nil, nil]
end
# Check to see if this is a IPv4 address in a tcp6/udp6 line.
# If so, don't put brackets around the IP or URI won't know how
# to properly handle it.
# example: f000000000000000 tcp6 0 0 127.0.0.1.8005 *.* LISTEN
if ip_parser.ipv4?
ip_addr = URI("addr://#{ip6addr}:#{port}")
host = ip_addr.host
else
ip_addr = URI("addr://[#{ip6addr}]:#{port}")
host = ip_addr.host[1..ip_addr.host.size-2]
end
else
ip4addr = address
# In AIX the wildcard character is used to match all interfaces
ip4addr = '0.0.0.0' if ip4addr =~ /^\*$/
ip_addr = URI("addr://#{ip4addr}:#{port}")
host = ip_addr.host
end
[host, port.to_i]
end
end
# extract port information from netstat
class LinuxPorts < PortsInfo # rubocop:disable Metrics/ClassLength
ALLOWED_PROTOCOLS = %w{tcp tcp6 udp udp6}.freeze

View file

@ -66,6 +66,7 @@ class MockLoader
solaris11: { name: "solaris", family: 'solaris', release: '11', arch: 'i386'},
solaris10: { name: "solaris", family: 'solaris', release: '10', arch: 'i386'},
hpux: { name: 'hpux', family: 'hpux', release: 'B.11.31', arch: 'ia64'},
aix: { name: 'aix', family: 'aix', release: '7.2', arch: 'powerpc' },
undefined: { name: nil, family: nil, release: nil, arch: nil },
}
@ -243,6 +244,10 @@ class MockLoader
'ss -tulpen' => cmd.call('ss-tulpen'),
# ports on freebsd
'sockstat -46l' => cmd.call('sockstat'),
# ports on aix
'netstat -Aan | grep LISTEN' => cmd.call('netstat-aan'),
'rmsock f0000000000000001 tcpcb' => cmd.call('rmsock-f0001'),
'rmsock f0000000000000002 tcpcb' => cmd.call('rmsock-f0002'),
# packages on windows
'6785190b3df7291a7622b0b75b0217a9a78bd04690bc978df51ae17ec852a282' => cmd.call('get-item-property-package'),
# service status upstart on ubuntu

View file

@ -0,0 +1,2 @@
f0000000000000001 tcp4 0 0 *.22 *.* LISTEN
f0000000000000002 tcp6 0 0 *.22 *.* LISTEN

View file

@ -0,0 +1 @@
The socket f0000000000000001 is being held by proccess 123456 (sshd)

View file

@ -0,0 +1 @@
The socket f0000000000000002 is being held by proccess 654321 (sshd)

View file

@ -208,4 +208,18 @@ describe 'Inspec::Resources::Port' do
_(resource.protocols).must_equal []
_(resource.addresses).must_equal []
end
it 'verify port on aix' do
resource = MockLoader.new(:aix).load_resource('port', 22)
_(resource.listening?).must_equal true
_(resource.protocols).must_equal %w{ tcp tcp6 }
_(resource.addresses).must_equal ["0.0.0.0", "::"]
end
it 'verify not listening port on aix' do
resource = MockLoader.new(:aix).load_resource('port', 23)
_(resource.listening?).must_equal false
_(resource.protocols).must_equal []
_(resource.addresses).must_equal []
end
end