diff --git a/lib/resources/port.rb b/lib/resources/port.rb index 3eca62a37..3270e0cf8 100644 --- a/lib/resources/port.rb +++ b/lib/resources/port.rb @@ -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 diff --git a/test/helper.rb b/test/helper.rb index cdff1112a..e04881f87 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -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 diff --git a/test/unit/mock/cmd/netstat-aan b/test/unit/mock/cmd/netstat-aan new file mode 100644 index 000000000..fb9ed6378 --- /dev/null +++ b/test/unit/mock/cmd/netstat-aan @@ -0,0 +1,2 @@ +f0000000000000001 tcp4 0 0 *.22 *.* LISTEN +f0000000000000002 tcp6 0 0 *.22 *.* LISTEN diff --git a/test/unit/mock/cmd/rmsock-f0001 b/test/unit/mock/cmd/rmsock-f0001 new file mode 100644 index 000000000..36b349063 --- /dev/null +++ b/test/unit/mock/cmd/rmsock-f0001 @@ -0,0 +1 @@ +The socket f0000000000000001 is being held by proccess 123456 (sshd) diff --git a/test/unit/mock/cmd/rmsock-f0002 b/test/unit/mock/cmd/rmsock-f0002 new file mode 100644 index 000000000..43409c5ba --- /dev/null +++ b/test/unit/mock/cmd/rmsock-f0002 @@ -0,0 +1 @@ +The socket f0000000000000002 is being held by proccess 654321 (sshd) diff --git a/test/unit/resources/port_test.rb b/test/unit/resources/port_test.rb index bc86111c2..5cd040022 100644 --- a/test/unit/resources/port_test.rb +++ b/test/unit/resources/port_test.rb @@ -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