# 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: 53, proto: 'udp') 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 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 } end " def initialize(hostname, params = {}) @hostname = hostname @port = params[:port] || nil @proto = params[:proto] || nil @host_provider = nil if inspec.os.linux? @host_provider = LinuxHostProvider.new(inspec) elsif inspec.os.windows? @host_provider = WindowsHostProvider.new(inspec) else return skip_resource 'The `host` resource is not supported on your OS yet.' end end # if we get the IP adress, 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?(port = nil, proto = nil, timeout = nil) fail "Use `host` resource with host('#{@hostname}', port: #{port}, proto: '#{proto}') parameters." if !port.nil? || !proto.nil? || !timeout.nil? ping.nil? ? false : ping end # returns all A records of the IP adress, 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) @ping_cache = @host_provider.ping(@hostname, @port, @proto) if !@host_provider.nil? 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 end class LinuxHostProvider < HostProvider # ping is difficult to achieve, since we are not sure def ping(hostname, _port = nil, _proto = nil) # fall back to ping, but we can only test ICMP packages with ping # therefore we have to skip the test, if we do not have everything on the node to run the test ping = inspec.command("ping -w 1 -c 1 #{hostname}") ping.exit_status.to_i != 0 ? false : true end def resolve(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 # extract ip adress resolve = /^\s*(?\S+)\s+(.*)\s*$/.match(cmd.stdout.chomp) [resolve[1]] if resolve 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) # TODO: abort if we cannot run it via udp return nil if proto == 'udp' # ICMP: Test-NetConnection www.microsoft.com # TCP and port: Test-NetConnection -ComputerName www.microsoft.com -RemotePort 80 request = "Test-NetConnection -ComputerName #{hostname}" request += " -RemotePort #{port}" unless port.nil? request += '| Select-Object -Property ComputerName, RemoteAddress, RemotePort, SourceAddress, PingSucceeded | ConvertTo-Json' p request request += '| Select-Object -Property ComputerName, PingSucceeded | ConvertTo-Json' cmd = inspec.command(request) begin ping = JSON.parse(cmd.stdout) rescue JSON::ParserError => _e return nil end ping['PingSucceeded'] 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