mirror of
https://github.com/inspec/inspec
synced 2024-11-26 22:50:36 +00:00
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:
parent
0994c63d48
commit
3e2450e703
5 changed files with 231 additions and 121 deletions
|
@ -55,13 +55,13 @@ module Inspec::Resources
|
|||
@protocol = params.fetch(:protocol, 'icmp')
|
||||
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
|
||||
if inspec.os.linux?
|
||||
@host_provider = LinuxHostProvider.new(inspec)
|
||||
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)
|
||||
elsif inspec.os.darwin?
|
||||
@host_provider = DarwinHostProvider.new(inspec)
|
||||
|
@ -147,6 +147,69 @@ module Inspec::Resources
|
|||
end
|
||||
|
||||
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)
|
||||
addresses = []
|
||||
|
||||
|
@ -191,28 +254,8 @@ module Inspec::Resources
|
|||
end
|
||||
|
||||
class DarwinHostProvider < UnixHostProvider
|
||||
def missing_requirements(protocol)
|
||||
missing = []
|
||||
|
||||
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,
|
||||
}
|
||||
def timeout
|
||||
'gtimeout'
|
||||
end
|
||||
|
||||
def resolve(hostname)
|
||||
|
@ -221,43 +264,6 @@ module Inspec::Resources
|
|||
end
|
||||
|
||||
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)
|
||||
resolve_with_getent(hostname)
|
||||
end
|
||||
|
|
|
@ -415,13 +415,22 @@ class MockLoader
|
|||
"modprobe --showconfig" => cmd.call('modprobe-config'),
|
||||
# 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'),
|
||||
# host resource: check to see if netcat is installed
|
||||
%{bash -c 'type "nc"'} => cmd.call('type-nc'),
|
||||
'type "nc"' => cmd.call('type-nc'),
|
||||
# host resource: netcat for TCP reachability check on linux
|
||||
'echo | nc -v -w 1 example.com 1234' => cmd.call('nc-example-com'),
|
||||
# host resource: TCP/UDP reachability check on linux
|
||||
%{bash -c 'type "nc"'} => empty.call,
|
||||
%{bash -c 'type "ncat"'} => empty.call,
|
||||
%{bash -c 'type "timeout"'} => empty.call,
|
||||
%{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
|
||||
'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
|
||||
'Test-NetConnection -ComputerName microsoft.com -WarningAction SilentlyContinue -RemotePort 1234| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json' => cmd.call('Test-NetConnection'),
|
||||
# postgres tests
|
||||
|
|
|
@ -55,6 +55,14 @@ describe 'Inspec::Resources::Host' do
|
|||
_(resource.to_s).must_equal 'Host example.com port 1234 proto tcp'
|
||||
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
|
||||
resource = MockLoader.new(:centos7).load_resource('host', 'example.com', port: 1234, protocol: 'tcp')
|
||||
_(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'
|
||||
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
|
||||
resource = MockLoader.new(:osx104).load_resource('host', 'example.com', port: 1234, protocol: 'tcp')
|
||||
_(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'
|
||||
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
|
||||
resource = MockLoader.new(:windows).load_resource('host', 'microsoft.com', port: 1234, protocol: 'tcp')
|
||||
_(resource.resolvable?).must_equal true
|
||||
|
@ -89,12 +113,31 @@ describe 'Inspec::Resources::Host' do
|
|||
end
|
||||
|
||||
describe Inspec::Resources::UnixHostProvider do
|
||||
let(:provider) { Inspec::Resources::UnixHostProvider.new(inspec) }
|
||||
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(:provider) { Inspec::Resources::UnixHostProvider.new(inspec) }
|
||||
let(:inspec) { mock('inspec-backend') }
|
||||
let(:v4_command) { mock('v4_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
|
||||
ipv4_command_output = <<-EOL
|
||||
a.cname.goes.here
|
||||
|
@ -169,16 +212,20 @@ EOL
|
|||
end
|
||||
|
||||
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
|
||||
command_output = "123.123.123.123 STREAM testdomain.com\n2607:f8b0:4004:805::200e STREAM\n"
|
||||
command = mock('getent_command')
|
||||
command.stubs(:stdout).returns(command_output)
|
||||
command.stubs(:exit_status).returns(0)
|
||||
|
||||
inspec = mock('inspec')
|
||||
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'])
|
||||
end
|
||||
|
||||
|
@ -186,84 +233,132 @@ EOL
|
|||
command = mock('getent_command')
|
||||
command.stubs(:exit_status).returns(1)
|
||||
|
||||
inspec = mock('inspec')
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Inspec::Resources::LinuxHostProvider do
|
||||
let(:provider) { Inspec::Resources::LinuxHostProvider.new(inspec) }
|
||||
let(:inspec) { mock('inspec-backend') }
|
||||
let(:nc_command) { mock('nc-command') }
|
||||
let(:ncat_command) { mock('ncat-command') }
|
||||
|
||||
before do
|
||||
provider.stubs(:inspec).returns(inspec)
|
||||
describe "#ping" do
|
||||
let(:command_response) { mock('response') }
|
||||
|
||||
before do
|
||||
strings_command.stubs(:exit_status).returns(0)
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
||||
provider.missing_requirements('tcp').must_equal([])
|
||||
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
|
||||
|
||||
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)
|
||||
inspec.stubs(:command).with('ncat').returns(ncat_command)
|
||||
ncat_command.stubs(:exist?).returns(true)
|
||||
describe 'bash without net redirects' do
|
||||
before do
|
||||
strings_command.stubs(:exit_status).returns(1)
|
||||
end
|
||||
|
||||
provider.missing_requirements('tcp').must_equal([])
|
||||
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([])
|
||||
end
|
||||
|
||||
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)
|
||||
inspec.stubs(:command).with('ncat').returns(ncat_command)
|
||||
ncat_command.stubs(:exist?).returns(true)
|
||||
it "returns an empty array if nc is not installed but ncat is installed" do
|
||||
nc_command.stubs(:exist?).returns(false)
|
||||
ncat_command.stubs(:exist?).returns(true)
|
||||
provider.missing_requirements('tcp').must_equal([])
|
||||
end
|
||||
|
||||
provider.missing_requirements('tcp').must_equal([])
|
||||
end
|
||||
it "returns an empty array if both nc and ncat are installed" do
|
||||
nc_command.stubs(:exist?).returns(true)
|
||||
ncat_command.stubs(:exist?).returns(true)
|
||||
provider.missing_requirements('tcp').must_equal([])
|
||||
end
|
||||
|
||||
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)
|
||||
inspec.stubs(:command).with('ncat').returns(ncat_command)
|
||||
ncat_command.stubs(:exist?).returns(false)
|
||||
|
||||
provider.missing_requirements('tcp').must_equal(['netcat must be installed'])
|
||||
it "returns a missing requirement when neither nc nor ncat are installed" do
|
||||
nc_command.stubs(:exist?).returns(false)
|
||||
ncat_command.stubs(:exist?).returns(false)
|
||||
provider.missing_requirements('tcp').must_equal(['netcat must be installed'])
|
||||
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'
|
||||
describe '#netcat_check_command' do
|
||||
before do
|
||||
strings_command.stubs(:exit_status).returns(1)
|
||||
end
|
||||
|
||||
it 'returns an ncat command when nc does not exist but ncat exists' do
|
||||
inspec.expects(:command).with('nc').returns(nc_command)
|
||||
inspec.expects(:command).with('ncat').returns(ncat_command)
|
||||
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)
|
||||
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
|
||||
|
||||
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)
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue