Host resource: use bash over netcat in Linux (#2607)

* Add support to use bash in host resource

Netcat's presence is widely regarded as a security issue, and thus not
always available. This solution first tries to use bash builtins and
timeout (from coreutils), so is less likely to require installing
additional packages.

* Darwin UDP support in host resource
* Host: use netcat first if available

Signed-off-by: João Vale <jpvale@gmail.com>
This commit is contained in:
João Vale 2018-03-07 13:39:27 +00:00 committed by Jared Quick
parent 0994c63d48
commit 3e2450e703
5 changed files with 231 additions and 121 deletions

View file

@ -55,13 +55,13 @@ module Inspec::Resources
@protocol = params.fetch(:protocol, 'icmp') @protocol = params.fetch(:protocol, 'icmp')
end end
return skip_resource 'Invalid protocol: only `tcp` and `icmp` protocols are support for the `host` resource.' unless
%w{icmp tcp}.include?(@protocol)
@host_provider = nil @host_provider = nil
if inspec.os.linux? if inspec.os.linux?
@host_provider = LinuxHostProvider.new(inspec) @host_provider = LinuxHostProvider.new(inspec)
elsif inspec.os.windows? elsif inspec.os.windows?
return skip_resource 'Invalid protocol: only `tcp` and `icmp` protocols are support for the `host` resource on your OS.' unless
%w{icmp tcp}.include?(@protocol)
@host_provider = WindowsHostProvider.new(inspec) @host_provider = WindowsHostProvider.new(inspec)
elsif inspec.os.darwin? elsif inspec.os.darwin?
@host_provider = DarwinHostProvider.new(inspec) @host_provider = DarwinHostProvider.new(inspec)
@ -147,6 +147,69 @@ module Inspec::Resources
end end
class UnixHostProvider < HostProvider class UnixHostProvider < HostProvider
def initialize(inspec)
super
@has_nc = inspec.command('nc').exist?
@has_ncat = inspec.command('ncat').exist?
@has_net_redirections = inspec.command("strings `which bash` | grep -qE '/dev/(tcp|udp)/'").exit_status == 0
end
def missing_requirements(protocol)
missing = []
if %w{tcp udp}.include?(protocol) && !@has_nc && !@has_ncat
if @has_net_redirections
missing << "#{timeout} (part of coreutils) or netcat must be installed" unless inspec.command(timeout).exist?
else
missing << 'netcat must be installed'
end
end
missing
end
def ping(hostname, port, protocol)
if %w{tcp udp}.include?(protocol)
if @has_nc || @has_ncat
resp = inspec.command(netcat_check_command(hostname, port, protocol))
else
resp = inspec.command("#{timeout} 1 bash -c \"< /dev/#{protocol}/#{hostname}/#{port}\"")
end
else
# fall back to ping, but we can only test ICMP packages with ping
resp = inspec.command("ping -w 1 -c 1 #{hostname}")
end
{
success: resp.exit_status.to_i.zero?,
connection: resp.stderr,
socket: resp.stdout,
}
end
def netcat_check_command(hostname, port, protocol)
if @has_nc
base_cmd = 'nc'
elsif @has_ncat
base_cmd = 'ncat'
else
return
end
if protocol == 'udp'
extra_flags = '-u'
else
extra_flags = ''
end
"echo | #{base_cmd} -v -w 1 #{extra_flags} #{hostname} #{port}"
end
def timeout
'timeout'
end
def resolve_with_dig(hostname) def resolve_with_dig(hostname)
addresses = [] addresses = []
@ -191,28 +254,8 @@ module Inspec::Resources
end end
class DarwinHostProvider < UnixHostProvider class DarwinHostProvider < UnixHostProvider
def missing_requirements(protocol) def timeout
missing = [] 'gtimeout'
if protocol == 'tcp'
missing << 'netcat must be installed' unless inspec.command('nc').exist?
end
missing
end
def ping(hostname, port, protocol)
if protocol == 'tcp'
resp = inspec.command("nc -vz -G 1 #{hostname} #{port}")
else
resp = inspec.command("ping -W 1 -c 1 #{hostname}")
end
{
success: resp.exit_status.to_i.zero?,
connection: resp.stderr,
socket: resp.stdout,
}
end end
def resolve(hostname) def resolve(hostname)
@ -221,43 +264,6 @@ module Inspec::Resources
end end
class LinuxHostProvider < UnixHostProvider class LinuxHostProvider < UnixHostProvider
def missing_requirements(protocol)
missing = []
if protocol == 'tcp' && (!inspec.command('nc').exist? && !inspec.command('ncat').exist?)
missing << 'netcat must be installed'
end
missing
end
def ping(hostname, port, protocol)
if protocol == 'tcp'
resp = inspec.command(tcp_check_command(hostname, port))
else
# fall back to ping, but we can only test ICMP packages with ping
resp = inspec.command("ping -w 1 -c 1 #{hostname}")
end
{
success: resp.exit_status.to_i.zero?,
connection: resp.stderr,
socket: resp.stdout,
}
end
def tcp_check_command(hostname, port)
if inspec.command('nc').exist?
base_cmd = 'nc'
elsif inspec.command('ncat').exist?
base_cmd = 'ncat'
else
return
end
"echo | #{base_cmd} -v -w 1 #{hostname} #{port}"
end
def resolve(hostname) def resolve(hostname)
resolve_with_getent(hostname) resolve_with_getent(hostname)
end end

View file

@ -415,13 +415,22 @@ class MockLoader
"modprobe --showconfig" => cmd.call('modprobe-config'), "modprobe --showconfig" => cmd.call('modprobe-config'),
# get-process cmdlet for processes resource # get-process cmdlet for processes resource
'$Proc = Get-Process -IncludeUserName | Where-Object {$_.Path -ne $null } | Select-Object PriorityClass,Id,CPU,PM,VirtualMemorySize,NPM,SessionId,Responding,StartTime,TotalProcessorTime,UserName,Path | ConvertTo-Csv -NoTypeInformation;$Proc.Replace("""","").Replace("`r`n","`n")' => cmd.call('get-process_processes'), '$Proc = Get-Process -IncludeUserName | Where-Object {$_.Path -ne $null } | Select-Object PriorityClass,Id,CPU,PM,VirtualMemorySize,NPM,SessionId,Responding,StartTime,TotalProcessorTime,UserName,Path | ConvertTo-Csv -NoTypeInformation;$Proc.Replace("""","").Replace("`r`n","`n")' => cmd.call('get-process_processes'),
# host resource: check to see if netcat is installed # host resource: TCP/UDP reachability check on linux
%{bash -c 'type "nc"'} => cmd.call('type-nc'), %{bash -c 'type "nc"'} => empty.call,
'type "nc"' => cmd.call('type-nc'), %{bash -c 'type "ncat"'} => empty.call,
# host resource: netcat for TCP reachability check on linux %{bash -c 'type "timeout"'} => empty.call,
'echo | nc -v -w 1 example.com 1234' => cmd.call('nc-example-com'), %{strings `which bash` | grep -qE '/dev/(tcp|udp)/'} => empty.call,
%{echo | nc -v -w 1 -u example.com 1234} => empty.call,
%{echo | nc -v -w 1 example.com 1234} => empty.call,
'timeout 1 bash -c "< /dev/tcp/example.com/1234"' => empty.call,
'timeout 1 bash -c "< /dev/udp/example.com/1234"' => empty.call,
# host resource: netcat for TCP reachability check on darwin # host resource: netcat for TCP reachability check on darwin
'nc -vz -G 1 example.com 1234' => cmd.call('nc-example-com'), 'type "nc"' => empty.call,
'type "ncat"' => empty.call,
'type "gtimeout"' => empty.call,
'nc -vz -G 1 example.com 1234' => empty.call,
'gtimeout 1 bash -c "< /dev/tcp/example.com/1234"' => empty.call,
'gtimeout 1 bash -c "< /dev/udp/example.com/1234"' => empty.call,
# host resource: test-netconnection for reachability check on windows # host resource: test-netconnection for reachability check on windows
'Test-NetConnection -ComputerName microsoft.com -WarningAction SilentlyContinue -RemotePort 1234| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json' => cmd.call('Test-NetConnection'), 'Test-NetConnection -ComputerName microsoft.com -WarningAction SilentlyContinue -RemotePort 1234| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json' => cmd.call('Test-NetConnection'),
# postgres tests # postgres tests

View file

@ -55,6 +55,14 @@ describe 'Inspec::Resources::Host' do
_(resource.to_s).must_equal 'Host example.com port 1234 proto tcp' _(resource.to_s).must_equal 'Host example.com port 1234 proto tcp'
end end
it 'check host udp on ubuntu' do
resource = MockLoader.new(:ubuntu1404).load_resource('host', 'example.com', port: 1234, protocol: 'udp')
_(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
_(resource.to_s).must_equal 'Host example.com port 1234 proto udp'
end
it 'check host tcp on centos 7' do it 'check host tcp on centos 7' do
resource = MockLoader.new(:centos7).load_resource('host', 'example.com', port: 1234, protocol: 'tcp') resource = MockLoader.new(:centos7).load_resource('host', 'example.com', port: 1234, protocol: 'tcp')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
@ -63,6 +71,14 @@ describe 'Inspec::Resources::Host' do
_(resource.to_s).must_equal 'Host example.com port 1234 proto tcp' _(resource.to_s).must_equal 'Host example.com port 1234 proto tcp'
end end
it 'check host udp on centos 7' do
resource = MockLoader.new(:centos7).load_resource('host', 'example.com', port: 1234, protocol: 'udp')
_(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
_(resource.to_s).must_equal 'Host example.com port 1234 proto udp'
end
it 'check host tcp on darwin' do it 'check host tcp on darwin' do
resource = MockLoader.new(:osx104).load_resource('host', 'example.com', port: 1234, protocol: 'tcp') resource = MockLoader.new(:osx104).load_resource('host', 'example.com', port: 1234, protocol: 'tcp')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
@ -71,6 +87,14 @@ describe 'Inspec::Resources::Host' do
_(resource.to_s).must_equal 'Host example.com port 1234 proto tcp' _(resource.to_s).must_equal 'Host example.com port 1234 proto tcp'
end end
it 'check host udp on darwin' do
resource = MockLoader.new(:osx104).load_resource('host', 'example.com', port: 1234, protocol: 'udp')
_(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
_(resource.to_s).must_equal 'Host example.com port 1234 proto udp'
end
it 'check host tcp on windows' do it 'check host tcp on windows' do
resource = MockLoader.new(:windows).load_resource('host', 'microsoft.com', port: 1234, protocol: 'tcp') resource = MockLoader.new(:windows).load_resource('host', 'microsoft.com', port: 1234, protocol: 'tcp')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
@ -89,12 +113,31 @@ describe 'Inspec::Resources::Host' do
end end
describe Inspec::Resources::UnixHostProvider do describe Inspec::Resources::UnixHostProvider do
describe '#resolve_with_dig' do
let(:provider) { Inspec::Resources::UnixHostProvider.new(inspec) } let(:provider) { Inspec::Resources::UnixHostProvider.new(inspec) }
let(:inspec) { mock('inspec-backend') } let(:inspec) { mock('inspec-backend') }
let(:nc_command) { mock('nc-command') }
let(:ncat_command) { mock('ncat-command') }
let(:timeout_command) { mock("timeout-command") }
let(:strings_command) { mock("strings-command") }
before do
inspec.stubs(:command).with('nc').returns(nc_command)
inspec.stubs(:command).with('ncat').returns(ncat_command)
inspec.stubs(:command).with('timeout').returns(timeout_command)
inspec.stubs(:command).with('gtimeout').returns(timeout_command)
inspec.stubs(:command).with("strings `which bash` | grep -qE '/dev/(tcp|udp)/'").returns(strings_command)
end
describe '#resolve_with_dig' do
let(:v4_command) { mock('v4_command') } let(:v4_command) { mock('v4_command') }
let(:v6_command) { mock('v6_command') } let(:v6_command) { mock('v6_command') }
before do
strings_command.stubs(:exit_status).returns(0)
nc_command.stubs(:exist?).returns(false)
ncat_command.stubs(:exist?).returns(false)
end
it 'returns an array of IP addresses' do it 'returns an array of IP addresses' do
ipv4_command_output = <<-EOL ipv4_command_output = <<-EOL
a.cname.goes.here a.cname.goes.here
@ -169,16 +212,20 @@ EOL
end end
describe '#resolve_with_getent' do describe '#resolve_with_getent' do
before do
strings_command.stubs(:exit_status).returns(0)
nc_command.stubs(:exist?).returns(false)
ncat_command.stubs(:exist?).returns(false)
end
it 'returns an array of IP addresses when successful' do it 'returns an array of IP addresses when successful' do
command_output = "123.123.123.123 STREAM testdomain.com\n2607:f8b0:4004:805::200e STREAM\n" command_output = "123.123.123.123 STREAM testdomain.com\n2607:f8b0:4004:805::200e STREAM\n"
command = mock('getent_command') command = mock('getent_command')
command.stubs(:stdout).returns(command_output) command.stubs(:stdout).returns(command_output)
command.stubs(:exit_status).returns(0) command.stubs(:exit_status).returns(0)
inspec = mock('inspec')
inspec.stubs(:command).with('getent ahosts testdomain.com').returns(command) inspec.stubs(:command).with('getent ahosts testdomain.com').returns(command)
provider = Inspec::Resources::UnixHostProvider.new(inspec)
provider.resolve_with_getent('testdomain.com').must_equal(['123.123.123.123', '2607:f8b0:4004:805::200e']) provider.resolve_with_getent('testdomain.com').must_equal(['123.123.123.123', '2607:f8b0:4004:805::200e'])
end end
@ -186,84 +233,132 @@ EOL
command = mock('getent_command') command = mock('getent_command')
command.stubs(:exit_status).returns(1) command.stubs(:exit_status).returns(1)
inspec = mock('inspec')
inspec.stubs(:command).with('getent ahosts testdomain.com').returns(command) inspec.stubs(:command).with('getent ahosts testdomain.com').returns(command)
provider = Inspec::Resources::UnixHostProvider.new(inspec)
provider.resolve_with_getent('testdomain.com').must_be_nil provider.resolve_with_getent('testdomain.com').must_be_nil
end end
end end
end
describe Inspec::Resources::LinuxHostProvider do
let(:provider) { Inspec::Resources::LinuxHostProvider.new(inspec) } describe "#ping" do
let(:inspec) { mock('inspec-backend') } let(:command_response) { mock('response') }
let(:nc_command) { mock('nc-command') }
let(:ncat_command) { mock('ncat-command') }
before do before do
provider.stubs(:inspec).returns(inspec) strings_command.stubs(:exit_status).returns(0)
end
describe '#missing_requirements' do
it "returns an empty array if nc is installed but ncat is not installed" do
inspec.stubs(:command).with('nc').returns(nc_command)
nc_command.stubs(:exist?).returns(true)
inspec.stubs(:command).with('ncat').returns(ncat_command)
ncat_command.stubs(:exist?).returns(false) ncat_command.stubs(:exist?).returns(false)
command_response.stubs(:exit_status).returns('0')
command_response.stubs(:stdout).returns('foo')
command_response.stubs(:stderr).returns('bar')
end
it "calls netcat if available" do
nc_command.stubs(:exist?).returns(true)
inspec.expects(:command).with('echo | nc -v -w 1 example.com 1234').returns(command_response)
provider.ping('example.com', '1234', 'tcp')
end
it "uses bash if netcat not available" do
nc_command.stubs(:exist?).returns(false)
inspec.expects(:command).with('timeout 1 bash -c "< /dev/tcp/example.com/1234"').returns(command_response)
provider.ping('example.com', '1234', 'tcp')
end
it "uses bash if netcat not available on Darwin" do
nc_command.stubs(:exist?).returns(false)
inspec.expects(:command).with('gtimeout 1 bash -c "< /dev/tcp/example.com/1234"').returns(command_response)
darwin_provider = Inspec::Resources::DarwinHostProvider.new(inspec)
darwin_provider.ping('example.com', '1234', 'tcp')
end
end
describe '#missing_requirements' do
describe 'bash with net redirects and no netcat' do
before do
strings_command.stubs(:exit_status).returns(0)
nc_command.stubs(:exist?).returns(false)
ncat_command.stubs(:exist?).returns(false)
end
it "returns an empty array if timeout is available" do
timeout_command.stubs(:exist?).returns(true)
provider.missing_requirements('tcp').must_equal([])
end
it "returns a missing requirement when timeout is missing" do
timeout_command.stubs(:exist?).returns(false)
provider.missing_requirements('tcp').must_equal(['timeout (part of coreutils) or netcat must be installed'])
end
end
describe 'bash without net redirects' do
before do
strings_command.stubs(:exit_status).returns(1)
end
it "returns an empty array if nc is installed but ncat is not installed" do
nc_command.stubs(:exist?).returns(true)
ncat_command.stubs(:exist?).returns(false)
provider.missing_requirements('tcp').must_equal([]) provider.missing_requirements('tcp').must_equal([])
end end
it "returns an empty array if nc is not installed but ncat is installed" do it "returns an empty array if nc is not installed but ncat is installed" do
inspec.stubs(:command).with('nc').returns(nc_command)
nc_command.stubs(:exist?).returns(false) nc_command.stubs(:exist?).returns(false)
inspec.stubs(:command).with('ncat').returns(ncat_command)
ncat_command.stubs(:exist?).returns(true) ncat_command.stubs(:exist?).returns(true)
provider.missing_requirements('tcp').must_equal([]) provider.missing_requirements('tcp').must_equal([])
end end
it "returns an empty array if both nc and ncat are installed" do it "returns an empty array if both nc and ncat are installed" do
inspec.stubs(:command).with('nc').returns(nc_command)
nc_command.stubs(:exist?).returns(true) nc_command.stubs(:exist?).returns(true)
inspec.stubs(:command).with('ncat').returns(ncat_command)
ncat_command.stubs(:exist?).returns(true) ncat_command.stubs(:exist?).returns(true)
provider.missing_requirements('tcp').must_equal([]) provider.missing_requirements('tcp').must_equal([])
end end
it "returns a missing requirement when neither nc nor ncat are installed" do it "returns a missing requirement when neither nc nor ncat are installed" do
inspec.stubs(:command).with('nc').returns(nc_command)
nc_command.stubs(:exist?).returns(false) nc_command.stubs(:exist?).returns(false)
inspec.stubs(:command).with('ncat').returns(ncat_command)
ncat_command.stubs(:exist?).returns(false) ncat_command.stubs(:exist?).returns(false)
provider.missing_requirements('tcp').must_equal(['netcat must be installed']) provider.missing_requirements('tcp').must_equal(['netcat must be installed'])
end end
end end
describe '#tcp_check_command' do
it 'returns an nc command when nc exists' do
inspec.expects(:command).with('nc').returns(nc_command)
nc_command.expects(:exist?).returns(true)
provider.tcp_check_command('foo', 1234).must_equal 'echo | nc -v -w 1 foo 1234'
end end
it 'returns an ncat command when nc does not exist but ncat exists' do describe '#netcat_check_command' do
inspec.expects(:command).with('nc').returns(nc_command) before do
inspec.expects(:command).with('ncat').returns(ncat_command) strings_command.stubs(:exit_status).returns(1)
end
it 'returns an nc command when nc exists tcp' do
nc_command.expects(:exist?).returns(true)
ncat_command.expects(:exist?).returns(false)
provider.netcat_check_command('foo', 1234, 'tcp').must_equal 'echo | nc -v -w 1 foo 1234'
end
it 'returns an nc command when nc exists udp' do
nc_command.expects(:exist?).returns(true)
ncat_command.expects(:exist?).returns(false)
provider.netcat_check_command('foo', 1234, 'udp').must_equal 'echo | nc -v -w 1 -u foo 1234'
end
it 'returns an ncat command when nc does not exist but ncat exists tcp' do
nc_command.expects(:exist?).returns(false) nc_command.expects(:exist?).returns(false)
ncat_command.expects(:exist?).returns(true) ncat_command.expects(:exist?).returns(true)
provider.tcp_check_command('foo', 1234).must_equal 'echo | ncat -v -w 1 foo 1234' provider.netcat_check_command('foo', 1234, 'tcp').must_equal 'echo | ncat -v -w 1 foo 1234'
end
it 'returns an ncat command when nc does not exist but ncat exists udp' do
nc_command.expects(:exist?).returns(false)
ncat_command.expects(:exist?).returns(true)
provider.netcat_check_command('foo', 1234, 'udp').must_equal 'echo | ncat -v -w 1 -u foo 1234'
end end
it 'returns nil if neither nc or ncat exist' do it 'returns nil if neither nc or ncat exist' do
inspec.expects(:command).with('nc').returns(nc_command)
inspec.expects(:command).with('ncat').returns(ncat_command)
nc_command.expects(:exist?).returns(false) nc_command.expects(:exist?).returns(false)
ncat_command.expects(:exist?).returns(false) ncat_command.expects(:exist?).returns(false)
provider.tcp_check_command('foo', 1234).must_be_nil provider.netcat_check_command('foo', 1234, 'tcp').must_be_nil
end end
end end
end end