863 lines
23 KiB
Ruby
863 lines
23 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'active_support/inflector'
|
|
require 'json'
|
|
require 'active_support/core_ext/hash'
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
class InvocationError < StandardError; end
|
|
class RequestRateTooHigh < StandardError; end
|
|
class InternalError < StandardError; end
|
|
class ServiceNotAvailable < StandardError; end
|
|
class ServiceOverloaded < StandardError; end
|
|
|
|
class Api
|
|
attr_reader :max_assessments, :current_assessments
|
|
|
|
def initialize
|
|
@max_assessments = 0
|
|
@current_assessments = 0
|
|
end
|
|
|
|
def request(name, params = {})
|
|
api_host = "api.ssllabs.com"
|
|
api_port = "443"
|
|
api_path = "/api/v2/"
|
|
user_agent = "Msf_ssllabs_scan"
|
|
|
|
name = name.to_s.camelize(:lower)
|
|
uri = api_path + name
|
|
cli = Rex::Proto::Http::Client.new(api_host, api_port, {}, true, 'TLS')
|
|
cli.connect
|
|
req = cli.request_cgi({
|
|
'uri' => uri,
|
|
'agent' => user_agent,
|
|
'method' => 'GET',
|
|
'vars_get' => params
|
|
})
|
|
res = cli.send_recv(req)
|
|
cli.close
|
|
|
|
if res && res.code.to_i == 200
|
|
@max_assessments = res.headers['X-Max-Assessments']
|
|
@current_assessments = res.headers['X-Current-Assessments']
|
|
r = JSON.load(res.body)
|
|
fail InvocationError, "API returned: #{r['errors']}" if r.key?('errors')
|
|
return r
|
|
end
|
|
|
|
case res.code.to_i
|
|
when 400
|
|
fail InvocationError
|
|
when 429
|
|
fail RequestRateTooHigh
|
|
when 500
|
|
fail InternalError
|
|
when 503
|
|
fail ServiceNotAvailable
|
|
when 529
|
|
fail ServiceOverloaded
|
|
else
|
|
fail StandardError, "HTTP error code #{r.code}", caller
|
|
end
|
|
end
|
|
|
|
def report_unused_attrs(type, unused_attrs)
|
|
unused_attrs.each do | attr |
|
|
# $stderr.puts "#{type} request returned unknown parameter #{attr}"
|
|
end
|
|
end
|
|
|
|
def info
|
|
obj, unused_attrs = Info.load request(:info)
|
|
report_unused_attrs('info', unused_attrs)
|
|
obj
|
|
end
|
|
|
|
def analyse(params = {})
|
|
obj, unused_attrs = Host.load request(:analyze, params)
|
|
report_unused_attrs('analyze', unused_attrs)
|
|
obj
|
|
end
|
|
|
|
def get_endpoint_data(params = {})
|
|
obj, unused_attrs = Endpoint.load request(:get_endpoint_data, params)
|
|
report_unused_attrs('get_endpoint_data', unused_attrs)
|
|
obj
|
|
end
|
|
|
|
def get_status_codes
|
|
obj, unused_attrs = StatusCodes.load request(:get_status_codes)
|
|
report_unused_attrs('get_status_codes', unused_attrs)
|
|
obj
|
|
end
|
|
end
|
|
|
|
class ApiObject
|
|
|
|
class << self;
|
|
attr_accessor :all_attributes
|
|
attr_accessor :fields
|
|
attr_accessor :lists
|
|
attr_accessor :refs
|
|
end
|
|
|
|
def self.inherited(base)
|
|
base.all_attributes = []
|
|
base.fields = []
|
|
base.lists = {}
|
|
base.refs = {}
|
|
end
|
|
|
|
def self.to_api_name(name)
|
|
name.to_s.gsub(/\?$/, '').camelize(:lower)
|
|
end
|
|
|
|
def self.to_attr_name(name)
|
|
name.to_s.gsub(/\?$/, '').underscore
|
|
end
|
|
|
|
def self.field_methods(name)
|
|
is_bool = name.to_s.end_with?('?')
|
|
attr_name = to_attr_name(name)
|
|
api_name = to_api_name(name)
|
|
class_eval <<-EOF, __FILE__, __LINE__
|
|
def #{attr_name}#{'?' if is_bool}
|
|
@#{api_name}
|
|
end
|
|
def #{attr_name}=(value)
|
|
@#{api_name} = value
|
|
end
|
|
EOF
|
|
end
|
|
|
|
def self.has_fields(*names)
|
|
names.each do |name|
|
|
@all_attributes << to_api_name(name)
|
|
@fields << to_api_name(name)
|
|
field_methods(name)
|
|
end
|
|
end
|
|
|
|
def self.has_objects_list(name, klass)
|
|
@all_attributes << to_api_name(name)
|
|
@lists[to_api_name(name)] = klass
|
|
field_methods(name)
|
|
end
|
|
|
|
def self.has_object_ref(name, klass)
|
|
@all_attributes << to_api_name(name)
|
|
@refs[to_api_name(name)] = klass
|
|
field_methods(name)
|
|
end
|
|
|
|
def self.load(attributes = {})
|
|
obj = self.new
|
|
unused_attrs = []
|
|
attributes.each do |name, value|
|
|
if @fields.include?(name)
|
|
obj.instance_variable_set("@#{name}", value)
|
|
elsif @lists.key?(name)
|
|
unless value.nil?
|
|
var = value.map do |v|
|
|
val, ua = @lists[name].load(v)
|
|
unused_attrs.concat ua
|
|
val
|
|
end
|
|
obj.instance_variable_set("@#{name}", var)
|
|
end
|
|
elsif @refs.key?(name)
|
|
unless value.nil?
|
|
val, ua = @refs[name].load(value)
|
|
unused_attrs.concat ua
|
|
obj.instance_variable_set("@#{name}", val)
|
|
end
|
|
else
|
|
unused_attrs << name
|
|
end
|
|
end
|
|
return obj, unused_attrs
|
|
end
|
|
|
|
def to_json(opts = {})
|
|
obj = {}
|
|
self.class.all_attributes.each do |api_name|
|
|
v = instance_variable_get("@#{api_name}")
|
|
obj[api_name] = v
|
|
end
|
|
obj.to_json
|
|
end
|
|
end
|
|
|
|
class Cert < ApiObject
|
|
has_fields :subject,
|
|
:commonNames,
|
|
:altNames,
|
|
:notBefore,
|
|
:notAfter,
|
|
:issuerSubject,
|
|
:sigAlg,
|
|
:issuerLabel,
|
|
:revocationInfo,
|
|
:crlURIs,
|
|
:ocspURIs,
|
|
:revocationStatus,
|
|
:crlRevocationStatus,
|
|
:ocspRevocationStatus,
|
|
:sgc?,
|
|
:validationType,
|
|
:issues,
|
|
:sct?,
|
|
:mustStaple,
|
|
:sha1Hash,
|
|
:pinSha256
|
|
|
|
def valid?
|
|
issues == 0
|
|
end
|
|
|
|
def invalid?
|
|
!valid?
|
|
end
|
|
end
|
|
|
|
class ChainCert < ApiObject
|
|
has_fields :subject,
|
|
:label,
|
|
:notBefore,
|
|
:notAfter,
|
|
:issuerSubject,
|
|
:issuerLabel,
|
|
:sigAlg,
|
|
:issues,
|
|
:keyAlg,
|
|
:keySize,
|
|
:keyStrength,
|
|
:revocationStatus,
|
|
:crlRevocationStatus,
|
|
:ocspRevocationStatus,
|
|
:raw,
|
|
:sha1Hash,
|
|
:pinSha256
|
|
|
|
def valid?
|
|
issues == 0
|
|
end
|
|
|
|
def invalid?
|
|
!valid?
|
|
end
|
|
end
|
|
|
|
class Chain < ApiObject
|
|
has_objects_list :certs, ChainCert
|
|
has_fields :issues
|
|
|
|
def valid?
|
|
issues == 0
|
|
end
|
|
|
|
def invalid?
|
|
!valid?
|
|
end
|
|
end
|
|
|
|
class Key < ApiObject
|
|
has_fields :size,
|
|
:strength,
|
|
:alg,
|
|
:debianFlaw?,
|
|
:q
|
|
|
|
def insecure?
|
|
debian_flaw? || q == 0
|
|
end
|
|
|
|
def secure?
|
|
!insecure?
|
|
end
|
|
end
|
|
|
|
class Protocol < ApiObject
|
|
has_fields :id,
|
|
:name,
|
|
:version,
|
|
:v2SuitesDisabled?,
|
|
:q
|
|
|
|
def insecure?
|
|
q == 0
|
|
end
|
|
|
|
def secure?
|
|
!insecure?
|
|
end
|
|
|
|
end
|
|
|
|
class Info < ApiObject
|
|
has_fields :engineVersion,
|
|
:criteriaVersion,
|
|
:clientMaxAssessments,
|
|
:maxAssessments,
|
|
:currentAssessments,
|
|
:messages,
|
|
:newAssessmentCoolOff
|
|
end
|
|
|
|
class SimClient < ApiObject
|
|
has_fields :id,
|
|
:name,
|
|
:platform,
|
|
:version,
|
|
:isReference?
|
|
end
|
|
|
|
class Simulation < ApiObject
|
|
has_object_ref :client, SimClient
|
|
has_fields :errorCode,
|
|
:attempts,
|
|
:protocolId,
|
|
:suiteId,
|
|
:kxInfo
|
|
|
|
def success?
|
|
error_code == 0
|
|
end
|
|
|
|
def error?
|
|
!success?
|
|
end
|
|
end
|
|
|
|
class SimDetails < ApiObject
|
|
has_objects_list :results, Simulation
|
|
end
|
|
|
|
class StatusCodes < ApiObject
|
|
has_fields :statusDetails
|
|
|
|
def [](name)
|
|
status_details[name]
|
|
end
|
|
end
|
|
|
|
class Suite < ApiObject
|
|
has_fields :id,
|
|
:name,
|
|
:cipherStrength,
|
|
:dhStrength,
|
|
:dhP,
|
|
:dhG,
|
|
:dhYs,
|
|
:ecdhBits,
|
|
:ecdhStrength,
|
|
:q
|
|
|
|
def insecure?
|
|
q == 0
|
|
end
|
|
|
|
def secure?
|
|
!insecure?
|
|
end
|
|
end
|
|
|
|
class Suites < ApiObject
|
|
has_objects_list :list, Suite
|
|
has_fields :preference?
|
|
end
|
|
|
|
class EndpointDetails < ApiObject
|
|
has_fields :hostStartTime
|
|
has_object_ref :key, Key
|
|
has_object_ref :cert, Cert
|
|
has_object_ref :chain, Chain
|
|
has_objects_list :protocols, Protocol
|
|
has_object_ref :suites, Suites
|
|
has_fields :serverSignature,
|
|
:prefixDelegation?,
|
|
:nonPrefixDelegation?,
|
|
:vulnBeast?,
|
|
:renegSupport,
|
|
:stsResponseHeader,
|
|
:stsMaxAge,
|
|
:stsSubdomains?,
|
|
:pkpResponseHeader,
|
|
:sessionResumption,
|
|
:compressionMethods,
|
|
:supportsNpn?,
|
|
:npnProtocols,
|
|
:sessionTickets,
|
|
:ocspStapling?,
|
|
:staplingRevocationStatus,
|
|
:staplingRevocationErrorMessage,
|
|
:sniRequired?,
|
|
:httpStatusCode,
|
|
:httpForwarding,
|
|
:supportsRc4?,
|
|
:forwardSecrecy,
|
|
:rc4WithModern?
|
|
has_object_ref :sims, SimDetails
|
|
has_fields :heartbleed?,
|
|
:heartbeat?,
|
|
:openSslCcs,
|
|
:poodle?,
|
|
:poodleTls,
|
|
:fallbackScsv?,
|
|
:freak?,
|
|
:hasSct,
|
|
:stsStatus,
|
|
:stsPreload,
|
|
:supportsAlpn,
|
|
:rc4Only,
|
|
:protocolIntolerance,
|
|
:miscIntolerance,
|
|
:openSSLLuckyMinus20,
|
|
:logjam,
|
|
:chaCha20Preference,
|
|
:hstsPolicy,
|
|
:hstsPreloads,
|
|
:hpkpPolicy,
|
|
:hpkpRoPolicy,
|
|
:drownHosts,
|
|
:drownErrors,
|
|
:drownVulnerable
|
|
end
|
|
|
|
class Endpoint < ApiObject
|
|
has_fields :ipAddress,
|
|
:serverName,
|
|
:statusMessage,
|
|
:statusDetails,
|
|
:statusDetailsMessage,
|
|
:grade,
|
|
:gradeTrustIgnored,
|
|
:hasWarnings?,
|
|
:isExceptional?,
|
|
:progress,
|
|
:duration,
|
|
:eta,
|
|
:delegation
|
|
has_object_ref :details, EndpointDetails
|
|
end
|
|
|
|
class Host < ApiObject
|
|
has_fields :host,
|
|
:port,
|
|
:protocol,
|
|
:isPublic?,
|
|
:status,
|
|
:statusMessage,
|
|
:startTime,
|
|
:testTime,
|
|
:engineVersion,
|
|
:criteriaVersion,
|
|
:cacheExpiryTime
|
|
has_objects_list :endpoints, Endpoint
|
|
has_fields :certHostnames
|
|
end
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'SSL Labs API Client',
|
|
'Description' => %q{
|
|
This module is a simple client for the SSL Labs APIs, designed for
|
|
SSL/TLS assessment during a penetration test.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'Denis Kolegov <dnkolegov[at]gmail.com>',
|
|
'Francois Chagnon' # ssllab.rb author (https://github.com/Shopify/ssllabs.rb)
|
|
],
|
|
'DefaultOptions' =>
|
|
{
|
|
'RPORT' => 443,
|
|
'SSL' => true,
|
|
}
|
|
))
|
|
register_options(
|
|
[
|
|
OptString.new('HOSTNAME', [true, 'The target hostname']),
|
|
OptInt.new('DELAY', [true, 'The delay in seconds between API requests', 5]),
|
|
OptBool.new('USECACHE', [true, 'Use cached results (if available), else force live scan', true]),
|
|
OptBool.new('GRADE', [true, 'Output only the hostname: grade', false]),
|
|
OptBool.new('IGNOREMISMATCH', [true, 'Proceed with assessments even when the server certificate doesn\'t match the assessment hostname', true])
|
|
])
|
|
end
|
|
|
|
def report_good(line)
|
|
print_good line
|
|
end
|
|
|
|
def report_warning(line)
|
|
print_warning line
|
|
end
|
|
|
|
def report_bad(line)
|
|
print_warning line
|
|
end
|
|
|
|
def report_status(line)
|
|
print_status line
|
|
end
|
|
|
|
def output_endpoint_data(r)
|
|
ssl_protocols = [
|
|
{ id: 771, name: "TLS", version: "1.2", secure: true, active: false },
|
|
{ id: 770, name: "TLS", version: "1.1", secure: true, active: false },
|
|
{ id: 769, name: "TLS", version: "1.0", secure: true, active: false },
|
|
{ id: 768, name: "SSL", version: "3.0", secure: false, active: false },
|
|
{ id: 2, name: "SSL", version: "2.0", secure: false, active: false }
|
|
]
|
|
|
|
report_status "-----------------------------------------------------------------"
|
|
report_status "Report for #{r.server_name} (#{r.ip_address})"
|
|
report_status "-----------------------------------------------------------------"
|
|
|
|
case r.grade.to_s
|
|
when "A+", "A", "A-"
|
|
report_good "Overall rating: #{r.grade}"
|
|
when "B"
|
|
report_warning "Overall rating: #{r.grade}"
|
|
when "C", "D", "E", "F"
|
|
report_bad "Overall rating: #{r.grade}"
|
|
when "M"
|
|
report_bad "Overall rating: #{r.grade} - Certificate name mismatch"
|
|
when "T"
|
|
report_bad "Overall rating: #{r.grade} - Server's certificate is not trusted"
|
|
end
|
|
|
|
report_warning "Grade is #{r.grade_trust_ignored}, if trust issues are ignored)" if r.grade.to_s != r.grade_trust_ignored.to_s
|
|
|
|
# Supported protocols
|
|
r.details.protocols.each do |i|
|
|
p = ssl_protocols.detect { |x| x[:id] == i.id }
|
|
p.store(:active, true) if p
|
|
end
|
|
|
|
ssl_protocols.each do |proto|
|
|
if proto[:active]
|
|
if proto[:secure]
|
|
report_good "#{proto[:name]} #{proto[:version]} - Yes"
|
|
else
|
|
report_bad "#{proto[:name]} #{proto[:version]} - Yes"
|
|
end
|
|
else
|
|
report_good "#{proto[:name]} #{proto[:version]} - No"
|
|
end
|
|
end
|
|
|
|
# Renegotioation
|
|
case
|
|
when r.details.reneg_support == 0
|
|
report_warning "Secure renegotiation is not supported"
|
|
when r.details.reneg_support[0] == 1
|
|
report_bad "Insecure client-initiated renegotiation is supported"
|
|
when r.details.reneg_support[1] == 1
|
|
report_good "Secure renegotiation is supported"
|
|
when r.details.reneg_support[2] == 1
|
|
report_warning "Secure client-initiated renegotiation is supported"
|
|
when r.details.reneg_support[3] == 1
|
|
report_warning "Server requires secure renegotiation support"
|
|
end
|
|
|
|
# BEAST
|
|
if r.details.vuln_beast?
|
|
report_bad "BEAST attack - Yes"
|
|
else
|
|
report_good "BEAST attack - No"
|
|
end
|
|
|
|
# POODLE (SSLv3)
|
|
if r.details.poodle?
|
|
report_bad "POODLE SSLv3 - Vulnerable"
|
|
else
|
|
report_good "POODLE SSLv3 - Not vulnerable"
|
|
end
|
|
|
|
# POODLE TLS
|
|
case r.details.poodle_tls
|
|
when -1
|
|
report_warning "POODLE TLS - Test failed"
|
|
when 0
|
|
report_warning "POODLE TLS - Unknown"
|
|
when 1
|
|
report_good "POODLE TLS - Not vulnerable"
|
|
when 2
|
|
report_bad "POODLE TLS - Vulnerable"
|
|
end
|
|
|
|
# Downgrade attack prevention
|
|
if r.details.fallback_scsv?
|
|
report_good "Downgrade attack prevention - Yes, TLS_FALLBACK_SCSV supported"
|
|
else
|
|
report_bad "Downgrade attack prevention - No, TLS_FALLBACK_SCSV not supported"
|
|
end
|
|
|
|
# Freak
|
|
if r.details.freak?
|
|
report_bad "Freak - Vulnerable"
|
|
else
|
|
report_good "Freak - Not vulnerable"
|
|
end
|
|
|
|
# RC4
|
|
if r.details.supports_rc4?
|
|
report_warning "RC4 - Server supports at least one RC4 suite"
|
|
else
|
|
report_good "RC4 - No"
|
|
end
|
|
|
|
# RC4 with modern browsers
|
|
report_warning "RC4 is used with modern clients" if r.details.rc4_with_modern?
|
|
|
|
# Heartbeat
|
|
if r.details.heartbeat?
|
|
report_status "Heartbeat (extension) - Yes"
|
|
else
|
|
report_status "Heartbeat (extension) - No"
|
|
end
|
|
|
|
# Heartbleed
|
|
if r.details.heartbleed?
|
|
report_bad "Heartbleed (vulnerability) - Yes"
|
|
else
|
|
report_good "Heartbleed (vulnerability) - No"
|
|
end
|
|
|
|
# OpenSSL CCS
|
|
case r.details.open_ssl_ccs
|
|
when -1
|
|
report_warning "OpenSSL CCS vulnerability (CVE-2014-0224) - Test failed"
|
|
when 0
|
|
report_warning "OpenSSL CCS vulnerability (CVE-2014-0224) - Unknown"
|
|
when 1
|
|
report_good "OpenSSL CCS vulnerability (CVE-2014-0224) - No"
|
|
when 2
|
|
report_bad "OpenSSL CCS vulnerability (CVE-2014-0224) - Possibly vulnerable, but not exploitable"
|
|
when 3
|
|
report_bad "OpenSSL CCS vulnerability (CVE-2014-0224) - Vulnerable and exploitable"
|
|
end
|
|
|
|
# Forward Secrecy
|
|
case
|
|
when r.details.forward_secrecy == 0
|
|
report_bad "Forward Secrecy - No"
|
|
when r.details.forward_secrecy[0] == 1
|
|
report_bad "Forward Secrecy - With some browsers"
|
|
when r.details.forward_secrecy[1] == 1
|
|
report_good "Forward Secrecy - With modern browsers"
|
|
when r.details.forward_secrecy[2] == 1
|
|
report_good "Forward Secrecy - Yes (with most browsers)"
|
|
end
|
|
|
|
# HSTS
|
|
if r.details.sts_response_header
|
|
str = "Strict Transport Security (HSTS) - Yes"
|
|
if r.details.sts_max_age && r.details.sts_max_age != -1
|
|
str += ":max-age=#{r.details.sts_max_age}"
|
|
end
|
|
str += ":includeSubdomains" if r.details.sts_subdomains?
|
|
report_good str
|
|
else
|
|
report_bad "Strict Transport Security (HSTS) - No"
|
|
end
|
|
|
|
# HPKP
|
|
if r.details.pkp_response_header
|
|
report_good "Public Key Pinning (HPKP) - Yes"
|
|
else
|
|
report_warning "Public Key Pinning (HPKP) - No"
|
|
end
|
|
|
|
# Compression
|
|
if r.details.compression_methods == 0
|
|
report_good "Compression - No"
|
|
elsif (r.details.session_tickets & 1) != 0
|
|
report_warning "Compression - Yes (Deflate)"
|
|
end
|
|
|
|
# Session Resumption
|
|
case r.details.session_resumption
|
|
when 0
|
|
print_status "Session resumption - No"
|
|
when 1
|
|
report_warning "Session resumption - No (IDs assigned but not accepted)"
|
|
when 2
|
|
print_status "Session resumption - Yes"
|
|
end
|
|
|
|
# Session Tickets
|
|
case
|
|
when r.details.session_tickets == 0
|
|
print_status "Session tickets - No"
|
|
when r.details.session_tickets[0] == 1
|
|
print_status "Session tickets - Yes"
|
|
when r.details.session_tickets[1] == 1
|
|
report_good "Session tickets - Implementation is faulty"
|
|
when r.details.session_tickets[2] == 1
|
|
report_warning "Session tickets - Server is intolerant to the extension"
|
|
end
|
|
|
|
# OCSP stapling
|
|
if r.details.ocsp_stapling?
|
|
print_status "OCSP Stapling - Yes"
|
|
else
|
|
print_status "OCSP Stapling - No"
|
|
end
|
|
|
|
# NPN
|
|
if r.details.supports_npn?
|
|
print_status "Next Protocol Negotiation (NPN) - Yes (#{r.details.npn_protocols})"
|
|
else
|
|
print_status "Next Protocol Negotiation (NPN) - No"
|
|
end
|
|
|
|
# SNI
|
|
print_status "SNI Required - Yes" if r.details.sni_required?
|
|
end
|
|
|
|
def output_grades_only(r)
|
|
r.endpoints.each do |e|
|
|
if e.status_message == "Ready"
|
|
print_status "Server: #{e.server_name} (#{e.ip_address}) - Grade:#{e.grade}"
|
|
else
|
|
print_status "Server: #{e.server_name} (#{e.ip_address} - Status:#{e.status_message}"
|
|
end
|
|
end
|
|
end
|
|
|
|
def output_common_info(r)
|
|
return unless r
|
|
print_status "Host: #{r.host}"
|
|
|
|
r.endpoints.each do |e|
|
|
print_status "\t #{e.ip_address}"
|
|
end
|
|
end
|
|
|
|
def output_result(r, grade)
|
|
return unless r
|
|
output_common_info(r)
|
|
if grade
|
|
output_grades_only(r)
|
|
else
|
|
r.endpoints.each do |e|
|
|
if e.status_message == "Ready"
|
|
output_endpoint_data(e)
|
|
else
|
|
print_status "#{e.status_message}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def output_testing_details(r)
|
|
return unless r.status == "IN_PROGRESS"
|
|
|
|
if r.endpoints.length == 1
|
|
print_status "#{r.host} (#{r.endpoints[0].ip_address}) - Progress #{[r.endpoints[0].progress, 0].max}% (#{r.endpoints[0].status_details_message})"
|
|
elsif r.endpoints.length > 1
|
|
in_progress_srv_num = 0
|
|
ready_srv_num = 0
|
|
pending_srv_num = 0
|
|
r.endpoints.each do |e|
|
|
case e.status_message.to_s
|
|
when "In progress"
|
|
in_progress_srv_num += 1
|
|
print_status "Scanned host: #{e.ip_address} (#{e.server_name})- #{[e.progress, 0].max}% complete (#{e.status_details_message})"
|
|
when "Pending"
|
|
pending_srv_num += 1
|
|
when "Ready"
|
|
ready_srv_num += 1
|
|
end
|
|
end
|
|
progress = ((ready_srv_num.to_f / (pending_srv_num + in_progress_srv_num + ready_srv_num)) * 100.0).round(0)
|
|
print_status "Ready: #{ready_srv_num}, In progress: #{in_progress_srv_num}, Pending: #{pending_srv_num}"
|
|
print_status "#{r.host} - Progress #{progress}%"
|
|
end
|
|
end
|
|
|
|
def valid_hostname?(hostname)
|
|
hostname =~ /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/
|
|
end
|
|
|
|
def run
|
|
delay = datastore['DELAY']
|
|
hostname = datastore['HOSTNAME']
|
|
unless valid_hostname?(hostname)
|
|
print_status "Invalid hostname"
|
|
return
|
|
end
|
|
|
|
usecache = datastore['USECACHE']
|
|
grade = datastore['GRADE']
|
|
|
|
# Use cached results
|
|
if usecache
|
|
from_cache = 'on'
|
|
start_new = 'off'
|
|
else
|
|
from_cache = 'off'
|
|
start_new = 'on'
|
|
end
|
|
|
|
# Ignore mismatch
|
|
ignore_mismatch = datastore['IGNOREMISMATCH'] ? 'on' : 'off'
|
|
|
|
api = Api.new
|
|
info = api.info
|
|
print_status "SSL Labs API info"
|
|
print_status "API version: #{info.engine_version}"
|
|
print_status "Evaluation criteria: #{info.criteria_version}"
|
|
print_status "Running assessments: #{info.current_assessments} (max #{info.max_assessments})"
|
|
|
|
if api.current_assessments >= api.max_assessments
|
|
print_status "Too many active assessments"
|
|
return
|
|
end
|
|
|
|
if usecache
|
|
r = api.analyse(host: hostname, fromCache: from_cache, ignoreMismatch: ignore_mismatch, all: 'done')
|
|
else
|
|
r = api.analyse(host: hostname, startNew: start_new, ignoreMismatch: ignore_mismatch, all: 'done')
|
|
end
|
|
|
|
loop do
|
|
case r.status
|
|
when "DNS"
|
|
print_status "Server: #{r.host} - #{r.status_message}"
|
|
when "IN_PROGRESS"
|
|
output_testing_details(r)
|
|
when "READY"
|
|
output_result(r, grade)
|
|
return
|
|
when "ERROR"
|
|
print_error "#{r.status_message}"
|
|
return
|
|
else
|
|
print_error "Unknown assessment status"
|
|
return
|
|
end
|
|
sleep delay
|
|
r = api.analyse(host: hostname, all: 'done')
|
|
end
|
|
|
|
rescue RequestRateTooHigh
|
|
print_error "Request rate is too high, please slow down"
|
|
rescue InternalError
|
|
print_error "Service encountered an error, sleep 5 minutes"
|
|
rescue ServiceNotAvailable
|
|
print_error "Service is not available, sleep 15 minutes"
|
|
rescue ServiceOverloaded
|
|
print_error "Service is overloaded, sleep 30 minutes"
|
|
rescue
|
|
print_error "Invalid parameters"
|
|
end
|
|
end
|