Change host resource to use getent ahosts on Linux (#2002)

* Change host resource to use getent ahosts on Linux

In InSpec 1.31, we changed the `host` resource to use `dig` instead of `getent
hosts` for name resolution because `getent hosts` does not return all entries
(only the first v6 entry if it exists, then the first v4 entry) and we wanted to
keep the Darwin and Linux implementation as close as possible. Unfortunately,
this affected users' ability to do resolution checks for entried stored in their
/etc/hosts file.

This change goes back to using `getent` for Linux and changes to `getent ahosts`
which returns both v4 and v6 records. Additionally, the Darwin provider's dig
implementation was reordered to return v4 addresses before v6 addresses to be
consistent with how `getent ahosts` returns records.

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

* Update unit tests for resolve_with_getent with proper output

Signed-off-by: Adam Leff <adam@leff.co>
This commit is contained in:
Adam Leff 2017-07-11 15:32:52 -04:00 committed by Dominik Richter
parent fb5e5c54e3
commit 1ea06ac3ea
5 changed files with 45 additions and 27 deletions

View file

@ -148,13 +148,6 @@ module Inspec::Resources
def resolve_with_dig(hostname) def resolve_with_dig(hostname)
addresses = [] addresses = []
# look for IPv6 addresses
cmd = inspec.command("dig +short AAAA #{hostname}")
cmd.stdout.lines.each do |line|
matched = line.chomp.match(Resolv::IPv6::Regex)
addresses << matched.to_s unless matched.nil?
end
# look for IPv4 addresses # look for IPv4 addresses
cmd = inspec.command("dig +short A #{hostname}") cmd = inspec.command("dig +short A #{hostname}")
cmd.stdout.lines.each do |line| cmd.stdout.lines.each do |line|
@ -162,17 +155,36 @@ module Inspec::Resources
addresses << matched.to_s unless matched.nil? addresses << matched.to_s unless matched.nil?
end end
# look for IPv6 addresses
cmd = inspec.command("dig +short AAAA #{hostname}")
cmd.stdout.lines.each do |line|
matched = line.chomp.match(Resolv::IPv6::Regex)
addresses << matched.to_s unless matched.nil?
end
addresses.empty? ? nil : addresses addresses.empty? ? nil : addresses
end end
def resolve_with_getent(hostname) def resolve_with_getent(hostname)
# TODO: we rely on getent hosts for now, but it prefers to return IPv6, only then IPv4 cmd = inspec.command("getent ahosts #{hostname}")
cmd = inspec.command("getent hosts #{hostname}") return nil unless cmd.exit_status.to_i.zero?
return nil if cmd.exit_status.to_i != 0
# extract ip adress # getent ahosts output is formatted like so:
resolve = /^\s*(?<ip>\S+)\s+(.*)\s*$/.match(cmd.stdout.chomp) # $ getent ahosts www.google.com
[resolve[1]] if resolve # 172.217.8.4 STREAM www.google.com
# 172.217.8.4 DGRAM
# 172.217.8.4 RAW
# 2607:f8b0:4004:803::2004 STREAM
# 2607:f8b0:4004:803::2004 DGRAM
# 2607:f8b0:4004:803::2004 RAW
addresses = []
cmd.stdout.lines.each do |line|
ip, = line.split(/\s+/, 2)
next unless ip.match(Resolv::IPv4::Regex) || ip.match(Resolv::IPv6::Regex)
addresses << ip unless addresses.include?(ip)
end
addresses
end end
end end
@ -245,7 +257,7 @@ module Inspec::Resources
end end
def resolve(hostname) def resolve(hostname)
inspec.command('dig').exist? ? resolve_with_dig(hostname) : resolve_with_getent(hostname) resolve_with_getent(hostname)
end end
end end

View file

@ -268,7 +268,7 @@ class MockLoader
'Resolve-DnsName Type A microsoft.com | ConvertTo-Json' => cmd.call('Resolve-DnsName'), 'Resolve-DnsName Type A microsoft.com | ConvertTo-Json' => cmd.call('Resolve-DnsName'),
'Test-NetConnection -ComputerName microsoft.com -WarningAction SilentlyContinue| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json' => cmd.call('Test-NetConnection'), 'Test-NetConnection -ComputerName microsoft.com -WarningAction SilentlyContinue| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json' => cmd.call('Test-NetConnection'),
# host for Linux # host for Linux
'getent hosts example.com' => cmd.call('getent-hosts-example.com'), 'getent ahosts example.com' => cmd.call('getent-ahosts-example.com'),
'ping -w 1 -c 1 example.com' => cmd.call('ping-example.com'), 'ping -w 1 -c 1 example.com' => cmd.call('ping-example.com'),
# host for Darwin # host for Darwin
'host -t AAAA example.com' => cmd.call('host-AAAA-example.com'), 'host -t AAAA example.com' => cmd.call('host-AAAA-example.com'),

View file

@ -0,0 +1,6 @@
12.34.56.78 STREAM example.com
12.34.56.78 DGRAM
12.34.56.78 RAW
2606:2800:220:1:248:1893:25c8:1946 STREAM
2606:2800:220:1:248:1893:25c8:1946 DGRAM
2606:2800:220:1:248:1893:25c8:1946 RAW

View file

@ -1 +0,0 @@
2606:2800:220:1:248:1893:25c8:1946 example.com

View file

@ -11,21 +11,21 @@ describe 'Inspec::Resources::Host' do
resource = MockLoader.new(:ubuntu1404).load_resource('host', 'example.com') resource = MockLoader.new(:ubuntu1404).load_resource('host', 'example.com')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true _(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["2606:2800:220:1:248:1893:25c8:1946", "12.34.56.78"] _(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
end end
it 'check host ping on centos 7' do it 'check host ping on centos 7' do
resource = MockLoader.new(:centos7).load_resource('host', 'example.com') resource = MockLoader.new(:centos7).load_resource('host', 'example.com')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true _(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["2606:2800:220:1:248:1893:25c8:1946", "12.34.56.78"] _(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
end end
it 'check host ping on darwin' do it 'check host ping on darwin' do
resource = MockLoader.new(:osx104).load_resource('host', 'example.com') resource = MockLoader.new(:osx104).load_resource('host', 'example.com')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true _(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["2606:2800:220:1:248:1893:25c8:1946", "12.34.56.78"] _(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
end end
it 'check host ping on windows' do it 'check host ping on windows' do
@ -46,21 +46,21 @@ describe 'Inspec::Resources::Host' do
resource = MockLoader.new(:ubuntu1404).load_resource('host', 'example.com', port: 1234, protocol: 'tcp') resource = MockLoader.new(:ubuntu1404).load_resource('host', 'example.com', port: 1234, protocol: 'tcp')
_(resource.resolvable?).must_equal true _(resource.resolvable?).must_equal true
_(resource.reachable?).must_equal true _(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["2606:2800:220:1:248:1893:25c8:1946", "12.34.56.78"] _(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
end 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
_(resource.reachable?).must_equal true _(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["2606:2800:220:1:248:1893:25c8:1946", "12.34.56.78"] _(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
end 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
_(resource.reachable?).must_equal true _(resource.reachable?).must_equal true
_(resource.ipaddress).must_equal ["2606:2800:220:1:248:1893:25c8:1946", "12.34.56.78"] _(resource.ipaddress).must_equal ["12.34.56.78", "2606:2800:220:1:248:1893:25c8:1946"]
end end
it 'check host tcp on windows' do it 'check host tcp on windows' do
@ -101,7 +101,7 @@ EOL
v6_command.stubs(:stdout).returns(ipv6_command_output) v6_command.stubs(:stdout).returns(ipv6_command_output)
inspec.stubs(:command).with('dig +short AAAA testdomain.com').returns(v6_command) inspec.stubs(:command).with('dig +short AAAA testdomain.com').returns(v6_command)
inspec.stubs(:command).with('dig +short A testdomain.com').returns(v4_command) inspec.stubs(:command).with('dig +short A testdomain.com').returns(v4_command)
provider.resolve_with_dig('testdomain.com').must_equal(['2A03:2880:F112:83:FACE:B00C::25DE', '12.34.56.78']) provider.resolve_with_dig('testdomain.com').must_equal(['12.34.56.78', '2A03:2880:F112:83:FACE:B00C::25DE'])
end end
it 'returns only v4 addresses if no v6 addresses are available' do it 'returns only v4 addresses if no v6 addresses are available' do
@ -160,16 +160,16 @@ EOL
describe '#resolve_with_getent' do describe '#resolve_with_getent' do
it 'returns an array of IP addresses when successful' do it 'returns an array of IP addresses when successful' do
command_output = "2607:f8b0:4004:805::200e testdomain.com\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 = mock('inspec')
inspec.stubs(:command).with('getent hosts testdomain.com').returns(command) inspec.stubs(:command).with('getent ahosts testdomain.com').returns(command)
provider = Inspec::Resources::UnixHostProvider.new(inspec) provider = Inspec::Resources::UnixHostProvider.new(inspec)
provider.resolve_with_getent('testdomain.com').must_equal(['2607:f8b0:4004:805::200e']) provider.resolve_with_getent('testdomain.com').must_equal(['123.123.123.123', '2607:f8b0:4004:805::200e'])
end end
it 'returns nil if command is not successful' do it 'returns nil if command is not successful' do
@ -177,7 +177,7 @@ EOL
command.stubs(:exit_status).returns(1) command.stubs(:exit_status).returns(1)
inspec = mock('inspec') inspec = mock('inspec')
inspec.stubs(:command).with('getent hosts testdomain.com').returns(command) inspec.stubs(:command).with('getent ahosts testdomain.com').returns(command)
provider = Inspec::Resources::UnixHostProvider.new(inspec) provider = Inspec::Resources::UnixHostProvider.new(inspec)
provider.resolve_with_getent('testdomain.com').must_be_nil provider.resolve_with_getent('testdomain.com').must_be_nil
@ -232,6 +232,7 @@ describe Inspec::Resources::LinuxHostProvider do
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 describe '#tcp_check_command' do
it 'returns an nc command when nc exists' do it 'returns an nc command when nc exists' do
inspec.expects(:command).with('nc').returns(nc_command) inspec.expects(:command).with('nc').returns(nc_command)