2015-10-09 17:10:10 +00:00
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
# Usage:
# describe host('example.com') do
# it { should be_resolvable }
# it { should be_reachable }
2016-05-03 22:14:33 +00:00
# its('ipaddress') { should include '93.184.216.34' }
2015-10-09 17:10:10 +00:00
# end
#
# To verify a hostname with protocol and port
2017-06-09 16:18:51 +00:00
# describe host('example.com', port: 443, protocol: 'tcp') do
2015-10-09 17:10:10 +00:00
# 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
2017-07-05 14:45:30 +00:00
require 'resolv'
2016-03-08 18:06:55 +00:00
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 }
2017-07-05 14:45:30 +00:00
it { should be_resolvable }
its ( 'ipaddress' ) { should include '12.34.56.78' }
2016-03-08 18:06:55 +00:00
end
2016-02-23 22:18:23 +00:00
2017-06-09 16:18:51 +00:00
describe host ( 'example.com' , port : '80' , protocol : 'tcp' ) do
2016-02-23 22:18:23 +00:00
it { should be_reachable }
end
2016-03-08 18:06:55 +00:00
"
2017-06-09 16:18:51 +00:00
attr_reader :hostname , :port , :protocol
2016-03-08 18:06:55 +00:00
def initialize ( hostname , params = { } )
@hostname = hostname
2017-06-09 16:18:51 +00:00
@port = params [ :port ]
2016-03-08 18:06:55 +00:00
2017-06-09 16:18:51 +00:00
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 )
2017-04-13 15:32:04 +00:00
2016-03-08 18:06:55 +00:00
@host_provider = nil
if inspec . os . linux?
@host_provider = LinuxHostProvider . new ( inspec )
elsif inspec . os . windows?
@host_provider = WindowsHostProvider . new ( inspec )
2017-04-13 15:32:04 +00:00
elsif inspec . os . darwin?
@host_provider = DarwinHostProvider . new ( inspec )
2016-03-08 18:06:55 +00:00
else
return skip_resource 'The `host` resource is not supported on your OS yet.'
end
2017-06-09 16:18:51 +00:00
missing_requirements = @host_provider . missing_requirements ( protocol )
2017-11-21 07:49:41 +00:00
return skip_resource 'The following requirements are not met for this resource: ' \
" #{ missing_requirements . join ( ', ' ) } " unless missing_requirements . empty?
2017-06-09 16:18:51 +00:00
end
def proto
warn '[DEPRECATION] The `proto` method is deprecated. Use `protocol` instead.'
protocol
2015-11-27 13:02:38 +00:00
end
2015-10-09 17:10:10 +00:00
2016-02-23 22:18:23 +00:00
# if we get the IP address, the host is resolvable
2016-03-08 18:06:55 +00:00
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
2015-10-09 17:10:10 +00:00
2017-06-09 16:18:51 +00:00
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
2017-06-15 18:01:16 +00:00
def connection
ping [ :connection ]
end
def socket
ping [ :socket ]
2016-03-08 18:06:55 +00:00
end
2015-10-09 17:10:10 +00:00
2016-02-23 22:18:23 +00:00
# returns all A records of the IP address, will return an array
2016-03-08 18:06:55 +00:00
def ipaddress
resolve . nil? || resolve . empty? ? nil : resolve
end
2015-10-09 17:10:10 +00:00
2016-03-08 18:06:55 +00:00
def to_s
2017-10-04 20:42:56 +00:00
resource_name = " Host #{ hostname } "
resource_name += " port #{ port } proto #{ protocol } " if port
resource_name
2016-03-08 18:06:55 +00:00
end
2015-10-09 17:10:10 +00:00
2016-03-08 18:06:55 +00:00
private
2015-10-09 17:10:10 +00:00
2016-03-08 18:06:55 +00:00
def ping
return @ping_cache if defined? ( @ping_cache )
2017-06-09 16:18:51 +00:00
return { } if @host_provider . nil?
@ping_cache = @host_provider . ping ( hostname , port , protocol )
2016-03-08 18:06:55 +00:00
end
2015-10-09 17:10:10 +00:00
2016-03-08 18:06:55 +00:00
def resolve
return @ip_cache if defined? ( @ip_cache )
2017-06-09 16:18:51 +00:00
@ip_cache = @host_provider . resolve ( hostname ) if ! @host_provider . nil?
2016-03-08 18:06:55 +00:00
end
2015-10-09 17:10:10 +00:00
end
2016-03-08 18:06:55 +00:00
class HostProvider
attr_reader :inspec
def initialize ( inspec )
@inspec = inspec
end
2017-06-09 16:18:51 +00:00
def missing_requirements ( _protocol )
# each provider can return an array of missing requirements that can
# be enumerated in a skip_resource message
[ ]
end
2015-10-09 17:10:10 +00:00
end
2017-07-05 14:45:30 +00:00
class UnixHostProvider < HostProvider
def resolve_with_dig ( hostname )
addresses = [ ]
2017-07-11 19:32:52 +00:00
# look for IPv4 addresses
cmd = inspec . command ( " dig +short A #{ hostname } " )
2017-07-05 14:45:30 +00:00
cmd . stdout . lines . each do | line |
2017-07-11 19:32:52 +00:00
matched = line . chomp . match ( Resolv :: IPv4 :: Regex )
2017-07-05 14:45:30 +00:00
addresses << matched . to_s unless matched . nil?
end
2017-07-11 19:32:52 +00:00
# look for IPv6 addresses
cmd = inspec . command ( " dig +short AAAA #{ hostname } " )
2017-07-05 14:45:30 +00:00
cmd . stdout . lines . each do | line |
2017-07-11 19:32:52 +00:00
matched = line . chomp . match ( Resolv :: IPv6 :: Regex )
2017-07-05 14:45:30 +00:00
addresses << matched . to_s unless matched . nil?
end
addresses . empty? ? nil : addresses
end
def resolve_with_getent ( hostname )
2017-07-11 19:32:52 +00:00
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
2017-07-05 14:45:30 +00:00
2017-07-11 19:32:52 +00:00
addresses
2017-07-05 14:45:30 +00:00
end
end
class DarwinHostProvider < UnixHostProvider
2017-06-09 16:18:51 +00:00
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'
2017-04-13 15:32:04 +00:00
resp = inspec . command ( " nc -vz -G 1 #{ hostname } #{ port } " )
else
resp = inspec . command ( " ping -W 1 -c 1 #{ hostname } " )
end
2017-06-09 16:18:51 +00:00
{
success : resp . exit_status . to_i . zero? ,
2017-06-15 18:01:16 +00:00
connection : resp . stderr ,
socket : resp . stdout ,
2017-06-09 16:18:51 +00:00
}
2017-04-13 15:32:04 +00:00
end
def resolve ( hostname )
2017-07-05 14:45:30 +00:00
resolve_with_dig ( hostname )
2017-04-13 15:32:04 +00:00
end
end
2017-07-05 14:45:30 +00:00
class LinuxHostProvider < UnixHostProvider
2017-06-09 16:18:51 +00:00
def missing_requirements ( protocol )
missing = [ ]
2017-07-06 20:23:57 +00:00
if protocol == 'tcp' && ( ! inspec . command ( 'nc' ) . exist? && ! inspec . command ( 'ncat' ) . exist? )
2017-07-06 17:19:16 +00:00
missing << 'netcat must be installed'
2017-06-09 16:18:51 +00:00
end
missing
end
def ping ( hostname , port , protocol )
if protocol == 'tcp'
2017-07-06 17:19:16 +00:00
resp = inspec . command ( tcp_check_command ( hostname , port ) )
2017-06-09 16:18:51 +00:00
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? ,
2017-06-15 18:01:16 +00:00
connection : resp . stderr ,
socket : resp . stdout ,
2017-06-09 16:18:51 +00:00
}
2016-03-08 18:06:55 +00:00
end
2015-10-09 17:10:10 +00:00
2017-07-06 17:19:16 +00:00
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
2016-03-08 18:06:55 +00:00
def resolve ( hostname )
2017-07-11 19:32:52 +00:00
resolve_with_getent ( hostname )
2016-03-08 18:06:55 +00:00
end
2015-10-09 17:10:10 +00:00
end
2016-03-08 18:06:55 +00:00
# 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
2017-04-13 15:32:04 +00:00
def ping ( hostname , port = nil , _proto = nil )
2016-03-08 18:06:55 +00:00
# ICMP: Test-NetConnection www.microsoft.com
# TCP and port: Test-NetConnection -ComputerName www.microsoft.com -RemotePort 80
2017-05-30 20:18:31 +00:00
request = " Test-NetConnection -ComputerName #{ hostname } -WarningAction SilentlyContinue "
2016-03-08 18:06:55 +00:00
request += " -RemotePort #{ port } " unless port . nil?
2016-02-23 22:18:23 +00:00
request += '| Select-Object -Property ComputerName, TcpTestSucceeded, PingSucceeded | ConvertTo-Json'
2016-03-08 18:06:55 +00:00
cmd = inspec . command ( request )
begin
ping = JSON . parse ( cmd . stdout )
rescue JSON :: ParserError = > _e
2017-06-09 16:18:51 +00:00
return { }
2016-03-08 18:06:55 +00:00
end
2017-06-09 16:18:51 +00:00
{ success : port . nil? ? ping [ 'PingSucceeded' ] : ping [ 'TcpTestSucceeded' ] }
2015-10-09 17:10:10 +00:00
end
2016-03-08 18:06:55 +00:00
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
2015-10-09 17:10:10 +00:00
2016-03-08 18:06:55 +00:00
resolv = [ resolv ] unless resolv . is_a? ( Array )
resolv . map { | entry | entry [ 'IPAddress' ] }
2015-10-09 17:10:10 +00:00
end
end
end