mirror of
https://github.com/inspec/inspec
synced 2024-11-14 17:07:09 +00:00
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:
parent
0f19e40d3b
commit
e2fa0b5e73
4 changed files with 121 additions and 32 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
8
test/unit/mock/cmd/ss-tulpen
Normal file
8
test/unit/mock/cmd/ss-tulpen
Normal 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 <->
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue