04d305feb3
This updates the SSL Labs scanner to know about new additions to the API, and prevents the module from breaking again just because there is new JSON in the output. I couldn't figure out how to get the Api class to print messages normally, and there is some other output that needs to be added. But the module does work again.
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
|