mirror of
https://github.com/inspec/inspec
synced 2024-11-23 13:13:22 +00:00
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:
parent
54136ac408
commit
2a8d6e0e91
6 changed files with 141 additions and 1 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
2
test/unit/mock/cmd/netstat-aan
Normal file
2
test/unit/mock/cmd/netstat-aan
Normal file
|
@ -0,0 +1,2 @@
|
|||
f0000000000000001 tcp4 0 0 *.22 *.* LISTEN
|
||||
f0000000000000002 tcp6 0 0 *.22 *.* LISTEN
|
1
test/unit/mock/cmd/rmsock-f0001
Normal file
1
test/unit/mock/cmd/rmsock-f0001
Normal file
|
@ -0,0 +1 @@
|
|||
The socket f0000000000000001 is being held by proccess 123456 (sshd)
|
1
test/unit/mock/cmd/rmsock-f0002
Normal file
1
test/unit/mock/cmd/rmsock-f0002
Normal file
|
@ -0,0 +1 @@
|
|||
The socket f0000000000000002 is being held by proccess 654321 (sshd)
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue