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)
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
cmd = inspec.command("dig +short A #{hostname}")
cmd.stdout.lines.each do |line|
@ -162,17 +155,36 @@ module Inspec::Resources
addresses << matched.to_s unless matched.nil?
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
end
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 hosts #{hostname}")
return nil if cmd.exit_status.to_i != 0
cmd = inspec.command("getent ahosts #{hostname}")
return nil unless cmd.exit_status.to_i.zero?
# extract ip adress
resolve = /^\s*(?<ip>\S+)\s+(.*)\s*$/.match(cmd.stdout.chomp)
[resolve[1]] if resolve
# getent ahosts output is formatted like so:
# $ getent ahosts www.google.com
# 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
@ -245,7 +257,7 @@ module Inspec::Resources
end
def resolve(hostname)
inspec.command('dig').exist? ? resolve_with_dig(hostname) : resolve_with_getent(hostname)
resolve_with_getent(hostname)
end
end

View file

@ -268,7 +268,7 @@ class MockLoader
'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'),
# 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'),
# host for Darwin
'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.resolvable?).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
it 'check host ping on centos 7' do
resource = MockLoader.new(:centos7).load_resource('host', 'example.com')
_(resource.resolvable?).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
it 'check host ping on darwin' do
resource = MockLoader.new(:osx104).load_resource('host', 'example.com')
_(resource.resolvable?).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
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.resolvable?).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
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
_(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
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
_(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
it 'check host tcp on windows' do
@ -101,7 +101,7 @@ EOL
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 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
it 'returns only v4 addresses if no v6 addresses are available' do
@ -160,16 +160,16 @@ EOL
describe '#resolve_with_getent' 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.stubs(:stdout).returns(command_output)
command.stubs(:exit_status).returns(0)
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.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
it 'returns nil if command is not successful' do
@ -177,7 +177,7 @@ EOL
command.stubs(:exit_status).returns(1)
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.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'])
end
end
describe '#tcp_check_command' do
it 'returns an nc command when nc exists' do
inspec.expects(:command).with('nc').returns(nc_command)