mirror of
https://github.com/inspec/inspec
synced 2024-11-27 07:00:39 +00:00
1ea06ac3ea
* 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>
298 lines
8.7 KiB
Ruby
298 lines
8.7 KiB
Ruby
# encoding: utf-8
|
||
# author: Christoph Hartmann
|
||
# author: Dominik Richter
|
||
|
||
# Usage:
|
||
# describe host('example.com') do
|
||
# it { should be_resolvable }
|
||
# it { should be_reachable }
|
||
# its('ipaddress') { should include '93.184.216.34' }
|
||
# end
|
||
#
|
||
# To verify a hostname with protocol and port
|
||
# describe host('example.com', port: 443, protocol: 'tcp') do
|
||
# it { should be_reachable }
|
||
# end
|
||
#
|
||
# We do not support the following serverspec syntax:
|
||
# describe host('example.com') do
|
||
# it { should be_reachable.with( :port => 22 ) }
|
||
# it { should be_reachable.with( :port => 22, :proto => 'tcp' ) }
|
||
# it { should be_reachable.with( :port => 53, :proto => 'udp' ) }
|
||
#
|
||
# it { should be_resolvable.by('hosts') }
|
||
# it { should be_resolvable.by('dns') }
|
||
# end
|
||
|
||
require 'resolv'
|
||
|
||
module Inspec::Resources
|
||
class Host < Inspec.resource(1)
|
||
name 'host'
|
||
desc 'Use the host InSpec audit resource to test the name used to refer to a specific host and its availability, including the Internet protocols and ports over which that host name should be available.'
|
||
example "
|
||
describe host('example.com') do
|
||
it { should be_reachable }
|
||
it { should be_resolvable }
|
||
its('ipaddress') { should include '12.34.56.78' }
|
||
end
|
||
|
||
describe host('example.com', port: '80', protocol: 'tcp') do
|
||
it { should be_reachable }
|
||
end
|
||
"
|
||
|
||
attr_reader :hostname, :port, :protocol
|
||
|
||
def initialize(hostname, params = {})
|
||
@hostname = hostname
|
||
@port = params[:port]
|
||
|
||
if params[:proto]
|
||
warn '[DEPRECATION] The `proto` parameter is deprecated. Use `protocol` instead.'
|
||
@protocol = params[:proto]
|
||
else
|
||
@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?
|
||
@host_provider = WindowsHostProvider.new(inspec)
|
||
elsif inspec.os.darwin?
|
||
@host_provider = DarwinHostProvider.new(inspec)
|
||
else
|
||
return skip_resource 'The `host` resource is not supported on your OS yet.'
|
||
end
|
||
|
||
missing_requirements = @host_provider.missing_requirements(protocol)
|
||
unless missing_requirements.empty?
|
||
return skip_resource "The following requirements are not met for this resource: #{missing_requirements.join(', ')}"
|
||
end
|
||
end
|
||
|
||
def proto
|
||
warn '[DEPRECATION] The `proto` method is deprecated. Use `protocol` instead.'
|
||
protocol
|
||
end
|
||
|
||
# if we get the IP address, the host is resolvable
|
||
def resolvable?(type = nil)
|
||
warn "The `host` resource ignores #{type} parameters. Continue to resolve host." if !type.nil?
|
||
resolve.nil? || resolve.empty? ? false : true
|
||
end
|
||
|
||
def reachable?
|
||
# ping checks do not require port or protocol
|
||
return ping.fetch(:success, false) if protocol == 'icmp'
|
||
|
||
# if either port or protocol are specified but not both, we cannot proceed.
|
||
if port.nil? || protocol.nil?
|
||
raise "Protocol required with port. Use `host` resource with host('#{hostname}', port: 1234, proto: 'tcp') parameters." if port.nil? || protocol.nil?
|
||
end
|
||
|
||
# perform the protocol-specific reachability test
|
||
ping.fetch(:success, false)
|
||
end
|
||
|
||
def connection
|
||
ping[:connection]
|
||
end
|
||
|
||
def socket
|
||
ping[:socket]
|
||
end
|
||
|
||
# returns all A records of the IP address, will return an array
|
||
def ipaddress
|
||
resolve.nil? || resolve.empty? ? nil : resolve
|
||
end
|
||
|
||
def to_s
|
||
"Host #{hostname}"
|
||
end
|
||
|
||
private
|
||
|
||
def ping
|
||
return @ping_cache if defined?(@ping_cache)
|
||
return {} if @host_provider.nil?
|
||
|
||
@ping_cache = @host_provider.ping(hostname, port, protocol)
|
||
end
|
||
|
||
def resolve
|
||
return @ip_cache if defined?(@ip_cache)
|
||
@ip_cache = @host_provider.resolve(hostname) if !@host_provider.nil?
|
||
end
|
||
end
|
||
|
||
class HostProvider
|
||
attr_reader :inspec
|
||
def initialize(inspec)
|
||
@inspec = inspec
|
||
end
|
||
|
||
def missing_requirements(_protocol)
|
||
# each provider can return an array of missing requirements that can
|
||
# be enumerated in a skip_resource message
|
||
[]
|
||
end
|
||
end
|
||
|
||
class UnixHostProvider < HostProvider
|
||
def resolve_with_dig(hostname)
|
||
addresses = []
|
||
|
||
# look for IPv4 addresses
|
||
cmd = inspec.command("dig +short A #{hostname}")
|
||
cmd.stdout.lines.each do |line|
|
||
matched = line.chomp.match(Resolv::IPv4::Regex)
|
||
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)
|
||
cmd = inspec.command("getent ahosts #{hostname}")
|
||
return nil unless cmd.exit_status.to_i.zero?
|
||
|
||
# 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
|
||
|
||
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,
|
||
}
|
||
end
|
||
|
||
def resolve(hostname)
|
||
resolve_with_dig(hostname)
|
||
end
|
||
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
|
||
end
|
||
|
||
# Windows
|
||
# TODO: UDP is not supported yey, we need a custom ps1 script to add udp support
|
||
# @see http://blogs.technet.com/b/josebda/archive/2015/04/18/windows-powershell-equivalents-for-common-networking-commands-ipconfig-ping-nslookup.aspx
|
||
# @see http://blogs.technet.com/b/heyscriptingguy/archive/2014/03/19/creating-a-port-scanner-with-windows-powershell.aspx
|
||
class WindowsHostProvider < HostProvider
|
||
def ping(hostname, port = nil, _proto = nil)
|
||
# ICMP: Test-NetConnection www.microsoft.com
|
||
# TCP and port: Test-NetConnection -ComputerName www.microsoft.com -RemotePort 80
|
||
request = "Test-NetConnection -ComputerName #{hostname} -WarningAction SilentlyContinue"
|
||
request += " -RemotePort #{port}" unless port.nil?
|
||
request += '| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json'
|
||
cmd = inspec.command(request)
|
||
|
||
begin
|
||
ping = JSON.parse(cmd.stdout)
|
||
rescue JSON::ParserError => _e
|
||
return {}
|
||
end
|
||
|
||
{ success: port.nil? ? ping['PingSucceeded'] : ping['TcpTestSucceeded'] }
|
||
end
|
||
|
||
def resolve(hostname)
|
||
cmd = inspec.command("Resolve-DnsName –Type A #{hostname} | ConvertTo-Json")
|
||
begin
|
||
resolv = JSON.parse(cmd.stdout)
|
||
rescue JSON::ParserError => _e
|
||
return nil
|
||
end
|
||
|
||
resolv = [resolv] unless resolv.is_a?(Array)
|
||
resolv.map { |entry| entry['IPAddress'] }
|
||
end
|
||
end
|
||
end
|