port resource: support ss instead of netstat (#2110)

* port resource: support ss instead of netstat

`netstat` is officially deprecated and is replaced with `ss`. This PR
changes the port resource to use `ss` if it's available on the target
system.

Signed-off-by: Adam Leff <adam@leff.co>

* Disable Metrics/ClassLength cop on the LinuxPorts class

Signed-off-by: Adam Leff <adam@leff.co>
This commit is contained in:
Adam Leff 2017-08-31 03:53:08 -04:00 committed by Dominik Richter
parent 0f19e40d3b
commit e2fa0b5e73
4 changed files with 121 additions and 32 deletions

View file

@ -264,10 +264,34 @@ module Inspec::Resources
end
# extract port information from netstat
class LinuxPorts < PortsInfo
class LinuxPorts < PortsInfo # rubocop:disable Metrics/ClassLength
ALLOWED_PROTOCOLS = %w{tcp tcp6 udp udp6}.freeze
def info
ports_via_ss || ports_via_netstat
end
def ports_via_ss
return nil unless inspec.command('ss').exist?
cmd = inspec.command('ss -tulpen')
return nil unless cmd.exit_status.to_i.zero?
ports = []
cmd.stdout.each_line do |line|
parsed_line = parse_ss_line(line)
ports << parsed_line unless parsed_line.nil?
end
ports
end
def ports_via_netstat
return nil unless inspec.command('netstat').exist?
cmd = inspec.command('netstat -tulpen')
return nil if cmd.exit_status.to_i != 0
return nil unless cmd.exit_status.to_i.zero?
ports = []
# parse all lines
@ -362,6 +386,66 @@ module Inspec::Resources
'pid' => pid,
}
end
def parse_ss_line(line)
parsed = line.split(/\s+/, 7)
# ss only returns "tcp" and "udp" as the protocol. However, netstat would return
# "tcp6" and "udp6" as necessary. In order to maintain backward compatibility, we
# will manually modify the protocol value if the line we're parsing is an IPv6
# entry.
process_info = parsed[6]
protocol = parsed[0]
protocol += '6' if process_info.include?('v6only:1')
return nil unless ALLOWED_PROTOCOLS.include?(protocol)
# parse the Local Address:Port
# examples:
# *:22
# :::22
# 10.0.2.15:1234
# ::ffff:10.0.2.15:9300
# fe80::a00:27ff:fe32:ed09%enp0s3:9200
parsed_net_address = parsed[4].match(/(\S+):(\*|\d+)$/)
return nil if parsed_net_address.nil?
host = parsed_net_address[1]
port = parsed_net_address[2]
return nil if host.nil? && port.nil?
# For backward compatibility with the netstat output, ensure the
# port is stored as an integer
port = port.to_i
# for those "v4-but-listed-in-v6" entries, strip off the
# leading IPv6 value at the beginning
# example: ::ffff:10.0.2.15:9200
host.delete!('::ffff:') if host.start_with?('::ffff:')
# if there's an interface name in the local address, which is common for
# IPv6 listeners, strip that out too.
# example: fe80::a00:27ff:fe32:ed09%enp0s3
host = host.split('%').first
# if host is "*", replace with "0.0.0.0" to maintain backward compatibility with
# the netstat-provided data
host = '0.0.0.0' if host == '*'
# parse the process name from the processes information
process_match = parsed[6].match(/users:\(\(\"(\S+)\"/)
process = process_match.nil? ? nil : process_match[1]
# parse the PID from the processes information
pid_match = parsed[6].match(/pid=(\d+)/)
pid = pid_match.nil? ? nil : pid_match[1].to_i
{
'port' => port,
'address' => host,
'protocol' => protocol,
'process' => process,
'pid' => pid,
}
end
end
# extracts information from sockstat

View file

@ -218,7 +218,9 @@ class MockLoader
# lsof formatted list of ports (should be quite cross platform)
'lsof -nP -i -FpctPn' => cmd.call('lsof-nP-i-FpctPn'),
# ports on linux
%{bash -c 'type "ss"'} => empty.call(), # allow the ss command to exist so the later mock is called
'netstat -tulpen' => cmd.call('netstat-tulpen'),
'ss -tulpen' => cmd.call('ss-tulpen'),
# ports on freebsd
'sockstat -46l' => cmd.call('sockstat'),
# packages on windows

View file

@ -0,0 +1,8 @@
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
udp UNCONN 0 0 *:68 *:* users:(("dhclient",pid=1146,fd=6)) ino:15168 sk:1 <->
tcp LISTEN 0 128 *:22 *:* users:(("sshd",pid=1222,fd=3)) ino:15973 sk:2 <->
tcp LISTEN 0 128 ::ffff:10.0.2.15:9200 :::* users:(("java",pid=1722,fd=125)) uid:112 ino:19543 sk:8 v6only:0 <->
tcp LISTEN 0 128 fe80::a00:27ff:fe32:ed09%enp0s3:9200 :::* users:(("java",pid=1722,fd=124)) uid:112 ino:19542 sk:9 v6only:1 <->
tcp LISTEN 0 128 ::ffff:10.0.2.15:9300 :::* users:(("java",pid=1722,fd=117)) uid:112 ino:19502 sk:a v6only:0 <->
tcp LISTEN 0 128 fe80::a00:27ff:fe32:ed09%enp0s3:9300 :::* users:(("java",pid=1722,fd=115)) uid:112 ino:19494 sk:b v6only:1 <->
tcp LISTEN 0 128 :::22 :::* users:(("sshd",pid=1222,fd=4)) ino:15982 sk:3 v6only:1 <->

View file

@ -10,19 +10,19 @@ describe 'Inspec::Resources::Port' do
resource = MockLoader.new(:ubuntu1404).load_resource('port', 22)
_(resource.listening?).must_equal true
_(resource.protocols).must_equal %w{ tcp tcp6 }
_(resource.pids).must_equal [1]
_(resource.pids).must_equal [1222]
_(resource.processes).must_equal ['sshd']
_(resource.addresses).must_equal ["0.0.0.0", "::"]
end
it 'lists all ports' do
resource = MockLoader.new(:ubuntu1404).load_resource('port')
_(resource.entries.length).must_equal 5
_(resource.entries.length).must_equal 7
_(resource.listening?).must_equal true
_(resource.protocols).must_equal %w{ tcp tcp6 udp }
_(resource.pids).must_equal [1, 2043, 545, 1234]
_(resource.processes).must_equal ['sshd', 'pidgin', 'rpcbind', 'java']
_(resource.addresses).must_equal ['0.0.0.0', '2601:1:ad80:1445::', '::', '192.168.1.123']
_(resource.protocols).must_equal %w{ udp tcp tcp6 }
_(resource.pids).must_equal [1146, 1222, 1722]
_(resource.processes).must_equal ['dhclient', 'sshd', 'java']
_(resource.addresses).must_equal ['0.0.0.0', '10.0.2.15', 'fe80::a00:27ff:fe32:ed09', '::']
end
it 'filter ports by conditions' do
@ -30,40 +30,35 @@ describe 'Inspec::Resources::Port' do
_(resource.entries.length).must_equal 1
_(resource.listening?).must_equal true
_(resource.protocols).must_equal ['udp']
_(resource.pids).must_equal [545]
_(resource.processes).must_equal ['rpcbind']
_(resource.pids).must_equal [1146]
_(resource.processes).must_equal ['dhclient']
_(resource.addresses).must_equal ['0.0.0.0']
end
it 'does not include an entry for a malformed IP address' do
# udp6 0 0 fe80::42:acff:fe11::123 :::* 0 54550 3335/ntpd
# the link-local IP is truncated and therefore invalid
resource = MockLoader.new(:ubuntu1404).load_resource('port', 123)
_(resource.entries.length).must_equal 0
end
it 'verify UDP port on Ubuntu 14.04' do
resource = MockLoader.new(:ubuntu1404).load_resource('port', 111)
resource = MockLoader.new(:ubuntu1404).load_resource('port', 68)
_(resource.entries.length).must_equal 1
_(resource.listening?).must_equal true
_(resource.protocols).must_equal %w{ udp }
_(resource.pids).must_equal [545]
_(resource.processes).must_equal ['rpcbind']
_(resource.addresses).must_equal ["0.0.0.0"]
_(resource.protocols).must_equal ['udp']
_(resource.pids).must_equal [1146]
_(resource.processes).must_equal ['dhclient']
_(resource.addresses).must_equal ['0.0.0.0']
end
it 'accepts the port as a string' do
resource = MockLoader.new(:ubuntu1404).load_resource('port', '111')
resource = MockLoader.new(:ubuntu1404).load_resource('port', '68')
_(resource.entries.length).must_equal 1
_(resource.listening?).must_equal true
_(resource.protocols).must_equal %w{ udp }
_(resource.pids).must_equal [545]
_(resource.processes).must_equal ['rpcbind']
_(resource.addresses).must_equal ["0.0.0.0"]
_(resource.protocols).must_equal ['udp']
_(resource.pids).must_equal [1146]
_(resource.processes).must_equal ['dhclient']
_(resource.addresses).must_equal ['0.0.0.0']
end
it 'properly handles a IPv4 address in a v6 listing' do
resource = MockLoader.new(:ubuntu1404).load_resource('port', 8005)
_(resource.protocols).must_equal %w{ tcp6 }
_(resource.addresses).must_equal ['192.168.1.123']
resource = MockLoader.new(:ubuntu1404).load_resource('port', 9200)
_(resource.protocols).must_equal %w{ tcp tcp6 }
_(resource.addresses).must_equal ['10.0.2.15', 'fe80::a00:27ff:fe32:ed09']
end
it 'verify port on MacOs x' do
@ -158,7 +153,7 @@ describe 'Inspec::Resources::Port' do
it 'verify port on wrlinux' do
resource = MockLoader.new(:wrlinux).load_resource('port', 22)
_(resource.listening?).must_equal true
_(resource.pids).must_equal [1]
_(resource.pids).must_equal [1222]
_(resource.protocols).must_equal %w{ tcp tcp6 }
_(resource.processes).must_equal ['sshd']
_(resource.addresses).must_equal ["0.0.0.0", "::"]
@ -177,7 +172,7 @@ describe 'Inspec::Resources::Port' do
resource = MockLoader.new(:ubuntu1404).load_resource('port', '0.0.0.0', 22)
_(resource.listening?).must_equal true
_(resource.protocols).must_equal %w{ tcp }
_(resource.pids).must_equal [1]
_(resource.pids).must_equal [1222]
_(resource.processes).must_equal ['sshd']
_(resource.addresses).must_equal ["0.0.0.0"]
end