Skip to content

Commit

Permalink
Uses netstat to detect open ports on AIX (#2210)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
cattywampus authored and arlimus committed Oct 10, 2017
1 parent 54136ac commit 2a8d6e0
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 1 deletion.
119 changes: 118 additions & 1 deletion lib/resources/port.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ def port_manager_for_os
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?
Expand Down Expand Up @@ -263,6 +265,121 @@ def lsof_parser(lsof_cmd)
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
Expand Down
5 changes: 5 additions & 0 deletions test/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
}

Expand Down Expand Up @@ -243,6 +244,10 @@ def md.directory?
'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
Expand Down
2 changes: 2 additions & 0 deletions test/unit/mock/cmd/netstat-aan
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
f0000000000000001 tcp4 0 0 *.22 *.* LISTEN
f0000000000000002 tcp6 0 0 *.22 *.* LISTEN
1 change: 1 addition & 0 deletions test/unit/mock/cmd/rmsock-f0001
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The socket f0000000000000001 is being held by proccess 123456 (sshd)
1 change: 1 addition & 0 deletions test/unit/mock/cmd/rmsock-f0002
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The socket f0000000000000002 is being held by proccess 654321 (sshd)
14 changes: 14 additions & 0 deletions test/unit/resources/port_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,18 @@
_(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

0 comments on commit 2a8d6e0

Please sign in to comment.