site-inspector 3.1.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +14 -10
- data/.rubocop_todo.yml +245 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +71 -0
- data/Gemfile +2 -0
- data/Guardfile +2 -0
- data/README.md +8 -14
- data/Rakefile +2 -0
- data/bin/site-inspector +16 -15
- data/lib/cliver/dependency_ext.rb +8 -5
- data/lib/data/well-known.yml +51 -0
- data/lib/site-inspector/domain.rb +61 -43
- data/lib/site-inspector/domain_parser.rb +11 -0
- data/lib/site-inspector/{checks → endpoint}/accessibility.rb +35 -25
- data/lib/site-inspector/{checks → endpoint}/check.rb +9 -3
- data/lib/site-inspector/endpoint/content.rb +151 -0
- data/lib/site-inspector/endpoint/cookies.rb +53 -0
- data/lib/site-inspector/{checks → endpoint}/dns.rb +52 -26
- data/lib/site-inspector/endpoint/headers.rb +55 -0
- data/lib/site-inspector/endpoint/hsts.rb +112 -0
- data/lib/site-inspector/{checks → endpoint}/https.rb +3 -1
- data/lib/site-inspector/endpoint/wappalyzer.rb +68 -0
- data/lib/site-inspector/endpoint/well_known.rb +95 -0
- data/lib/site-inspector/endpoint/whois.rb +56 -0
- data/lib/site-inspector/endpoint.rb +80 -44
- data/lib/site-inspector/formatter.rb +118 -0
- data/lib/site-inspector/version.rb +3 -1
- data/lib/site-inspector.rb +42 -31
- data/package-lock.json +1152 -0
- data/package.json +2 -1
- data/renovate.json +4 -0
- data/script/benchmark +8 -0
- data/script/cibuild +2 -2
- data/script/console +1 -1
- data/script/pa11y-version +2 -1
- data/script/release +11 -4
- data/script/update-well-known +18 -0
- data/site-inspector.gemspec +33 -19
- data/spec/cliver/dependency_ext_spec.rb +21 -0
- data/spec/fixtures/wappalyzer.json +125 -0
- data/spec/{site_inspector_domain_spec.rb → site_inspector/domain_spec.rb} +105 -43
- data/spec/{checks/site_inspector_endpoint_accessibility_spec.rb → site_inspector/endpoint/accessibility_spec.rb} +24 -20
- data/spec/{checks/site_inspector_endpoint_check_spec.rb → site_inspector/endpoint/check_spec.rb} +9 -7
- data/spec/{checks/site_inspector_endpoint_content_spec.rb → site_inspector/endpoint/content_spec.rb} +43 -24
- data/spec/{checks/site_inspector_endpoint_cookies_spec.rb → site_inspector/endpoint/cookies_spec.rb} +37 -22
- data/spec/site_inspector/endpoint/dns_spec.rb +262 -0
- data/spec/{checks/site_inspector_endpoint_headers_spec.rb → site_inspector/endpoint/headers_spec.rb} +10 -15
- data/spec/site_inspector/endpoint/hsts_spec.rb +92 -0
- data/spec/{checks/site_inspector_endpoint_https_spec.rb → site_inspector/endpoint/https_spec.rb} +12 -10
- data/spec/site_inspector/endpoint/wappalyzer_spec.rb +20 -0
- data/spec/site_inspector/endpoint/well_known_spec.rb +12 -0
- data/spec/site_inspector/endpoint/whois_spec.rb +27 -0
- data/spec/{site_inspector_endpoint_spec.rb → site_inspector/endpoint_spec.rb} +102 -45
- data/spec/site_inspector_spec.rb +67 -18
- data/spec/spec_helper.rb +11 -2
- metadata +178 -100
- data/.travis.yml +0 -9
- data/lib/site-inspector/cache.rb +0 -15
- data/lib/site-inspector/checks/content.rb +0 -74
- data/lib/site-inspector/checks/cookies.rb +0 -43
- data/lib/site-inspector/checks/headers.rb +0 -66
- data/lib/site-inspector/checks/hsts.rb +0 -77
- data/lib/site-inspector/checks/sniffer.rb +0 -64
- data/lib/site-inspector/disk_cache.rb +0 -40
- data/lib/site-inspector/rails_cache.rb +0 -11
- data/spec/checks/site_inspector_endpoint_dns_spec.rb +0 -182
- data/spec/checks/site_inspector_endpoint_hsts_spec.rb +0 -90
- data/spec/checks/site_inspector_endpoint_sniffer_spec.rb +0 -148
- data/spec/site_inspector_cache_spec.rb +0 -13
- data/spec/site_inspector_disk_cache_spec.rb +0 -37
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ipaddr'
|
|
4
|
+
|
|
1
5
|
class SiteInspector
|
|
2
6
|
class Endpoint
|
|
3
7
|
class Dns < Check
|
|
4
8
|
class LocalhostError < StandardError; end
|
|
5
9
|
|
|
10
|
+
# Record types fetched for #records. ANY queries are deprecated
|
|
11
|
+
# (RFC 8482) and most resolvers answer them with a single HINFO record.
|
|
12
|
+
RECORD_TYPES = %w[A AAAA CNAME MX DNSKEY].freeze
|
|
13
|
+
|
|
6
14
|
def self.resolver
|
|
7
15
|
require 'dnsruby'
|
|
8
16
|
@resolver ||= begin
|
|
@@ -12,20 +20,21 @@ class SiteInspector
|
|
|
12
20
|
end
|
|
13
21
|
end
|
|
14
22
|
|
|
15
|
-
def query(type = '
|
|
16
|
-
|
|
17
|
-
rescue Dnsruby::ResolvTimeout, Dnsruby::ServFail, Dnsruby::NXDomain
|
|
18
|
-
[]
|
|
23
|
+
def query(type = 'A')
|
|
24
|
+
lookup(host.to_s, type)
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
def records
|
|
22
|
-
@records ||= query
|
|
28
|
+
@records ||= RECORD_TYPES.flat_map { |type| query(type) }.uniq(&:to_s)
|
|
23
29
|
end
|
|
24
30
|
|
|
25
31
|
def record?(type)
|
|
26
|
-
records.any? { |record| record.type == type }
|
|
32
|
+
return true if records.any? { |record| record.type == type }
|
|
33
|
+
return false if RECORD_TYPES.include?(type.to_s)
|
|
34
|
+
|
|
35
|
+
query(type).any?
|
|
27
36
|
end
|
|
28
|
-
|
|
37
|
+
alias has_record? record?
|
|
29
38
|
|
|
30
39
|
def dnssec?
|
|
31
40
|
@dnssec ||= has_record? 'DNSKEY'
|
|
@@ -52,27 +61,36 @@ class SiteInspector
|
|
|
52
61
|
end
|
|
53
62
|
|
|
54
63
|
def google_apps?
|
|
55
|
-
@
|
|
64
|
+
@google_apps ||= records.any? do |record|
|
|
56
65
|
record.type == 'MX' && record.exchange.to_s =~ /google(mail)?\.com\.?\z/i
|
|
57
66
|
end
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
def localhost?
|
|
61
|
-
|
|
70
|
+
return false unless ip
|
|
71
|
+
|
|
72
|
+
IPAddr.new(ip).loopback?
|
|
73
|
+
rescue IPAddr::InvalidAddressError
|
|
74
|
+
false
|
|
62
75
|
end
|
|
63
76
|
|
|
77
|
+
# The host's first IPv4 address, falling back to IPv6
|
|
64
78
|
def ip
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
@ip ||= begin
|
|
80
|
+
record = records.find { |r| r.type == 'A' } || records.find { |r| r.type == 'AAAA' }
|
|
81
|
+
record&.address&.to_s
|
|
82
|
+
end
|
|
69
83
|
end
|
|
70
84
|
|
|
85
|
+
# The PTR hostname for #ip
|
|
71
86
|
def hostname
|
|
72
|
-
|
|
73
|
-
@hostname
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
return @hostname if defined?(@hostname)
|
|
88
|
+
return @hostname = nil unless ip
|
|
89
|
+
|
|
90
|
+
ptr = lookup(ip, 'PTR').find { |record| record.type == 'PTR' }
|
|
91
|
+
@hostname = ptr ? PublicSuffix.parse(ptr.domainname.to_s) : nil
|
|
92
|
+
rescue PublicSuffix::DomainInvalid
|
|
93
|
+
@hostname = nil
|
|
76
94
|
end
|
|
77
95
|
|
|
78
96
|
def cnames
|
|
@@ -87,19 +105,27 @@ class SiteInspector
|
|
|
87
105
|
|
|
88
106
|
def to_h
|
|
89
107
|
return { error: LocalhostError } if localhost?
|
|
108
|
+
|
|
90
109
|
{
|
|
91
|
-
dnssec:
|
|
92
|
-
ipv6:
|
|
93
|
-
cdn
|
|
94
|
-
cloud_provider
|
|
95
|
-
google_apps:
|
|
96
|
-
hostname:
|
|
97
|
-
ip:
|
|
110
|
+
dnssec: dnssec?,
|
|
111
|
+
ipv6: ipv6?,
|
|
112
|
+
cdn:,
|
|
113
|
+
cloud_provider:,
|
|
114
|
+
google_apps: google_apps?,
|
|
115
|
+
hostname: hostname.to_s,
|
|
116
|
+
ip:
|
|
98
117
|
}
|
|
99
118
|
end
|
|
100
119
|
|
|
101
120
|
private
|
|
102
121
|
|
|
122
|
+
def lookup(name, type)
|
|
123
|
+
SiteInspector::Endpoint::Dns.resolver.query(name, type).answer
|
|
124
|
+
rescue Dnsruby::ResolvTimeout, Dnsruby::ResolvError => e
|
|
125
|
+
SiteInspector.logger.warn e.message
|
|
126
|
+
[]
|
|
127
|
+
end
|
|
128
|
+
|
|
103
129
|
def data
|
|
104
130
|
@data ||= {}
|
|
105
131
|
end
|
|
@@ -118,7 +144,7 @@ class SiteInspector
|
|
|
118
144
|
haystack = load_data(type)
|
|
119
145
|
needle = haystack.find do |_name, domain|
|
|
120
146
|
cnames.any? do |cname|
|
|
121
|
-
|
|
147
|
+
[cname.tld, "#{cname.sld}.#{cname.tld}"].include? domain
|
|
122
148
|
end
|
|
123
149
|
end
|
|
124
150
|
|
|
@@ -126,7 +152,7 @@ class SiteInspector
|
|
|
126
152
|
return nil unless hostname
|
|
127
153
|
|
|
128
154
|
needle = haystack.find do |_name, domain|
|
|
129
|
-
|
|
155
|
+
[hostname.tld, "#{hostname.sld}.#{hostname.tld}"].include? domain
|
|
130
156
|
end
|
|
131
157
|
|
|
132
158
|
needle ? needle[0].to_sym : nil
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SiteInspector
|
|
4
|
+
class Endpoint
|
|
5
|
+
class Headers < Check
|
|
6
|
+
COMMON = %w[
|
|
7
|
+
strict-transport-security
|
|
8
|
+
content-security-policy
|
|
9
|
+
x-frame-options
|
|
10
|
+
server
|
|
11
|
+
x-xss-protection
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
COMMON.each do |header|
|
|
15
|
+
name = header.gsub(/^x-/, '').tr('-', '_')
|
|
16
|
+
|
|
17
|
+
define_method name do
|
|
18
|
+
headers[header]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
define_method "#{name}?" do
|
|
22
|
+
!!headers[header]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# more specific checks than presence of headers
|
|
27
|
+
def xss_protection?
|
|
28
|
+
xss_protection == '1; mode=block'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns an array of hashes of downcased key/value header pairs (or an empty hash)
|
|
32
|
+
def all
|
|
33
|
+
@all ||= response&.headers ? response.headers.transform_keys(&:downcase) : {}
|
|
34
|
+
end
|
|
35
|
+
alias headers all
|
|
36
|
+
|
|
37
|
+
def custom_headers
|
|
38
|
+
@custom_headers ||= all.select do |header|
|
|
39
|
+
header.start_with?('x-') && !COMMON.include?(header)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def [](header)
|
|
44
|
+
headers[header]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_h
|
|
48
|
+
return {} if endpoint.redirect?
|
|
49
|
+
|
|
50
|
+
hash = COMMON.to_h { |h| [h, headers[h] || false] }
|
|
51
|
+
hash.merge custom_headers
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SiteInspector
|
|
4
|
+
class Endpoint
|
|
5
|
+
# Utility parser for HSTS headers.
|
|
6
|
+
# RFC: http://tools.ietf.org/html/rfc6797
|
|
7
|
+
class Hsts < Check
|
|
8
|
+
STATUS_ENDPOINT = 'https://hstspreload.org/api/v2/status'
|
|
9
|
+
|
|
10
|
+
def valid?
|
|
11
|
+
return false if header.empty?
|
|
12
|
+
|
|
13
|
+
pairs.none? { |key, value| "#{key}#{value}" =~ /[\s'"]/ }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def max_age
|
|
17
|
+
pairs[:'max-age'].to_i
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def include_subdomains?
|
|
21
|
+
pairs.key?(:includesubdomains)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def preload?
|
|
25
|
+
pairs.key?(:preload)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def enabled?
|
|
29
|
+
return false unless max_age
|
|
30
|
+
|
|
31
|
+
max_age.positive?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Google's minimum max-age for automatic preloading
|
|
35
|
+
def preload_ready?
|
|
36
|
+
include_subdomains? && preload? && max_age >= 10_886_400
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
valid: valid?,
|
|
42
|
+
max_age:,
|
|
43
|
+
include_subdomains: include_subdomains?,
|
|
44
|
+
preload: preload?,
|
|
45
|
+
enabled: enabled?,
|
|
46
|
+
preload_ready: preload_ready?,
|
|
47
|
+
preload_list_status: preload_list['status']
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def headers
|
|
54
|
+
endpoint.headers
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def header
|
|
58
|
+
@header ||= [headers['strict-transport-security']].flatten.join('; ').squeeze(';')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def directives
|
|
62
|
+
@directives ||= header ? header.split(/\s*;\s*/) : []
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pairs
|
|
66
|
+
@pairs ||= begin
|
|
67
|
+
pairs = {}
|
|
68
|
+
directives.each do |directive|
|
|
69
|
+
key, value = directive.downcase.split('=')
|
|
70
|
+
|
|
71
|
+
if /".*"/.match?(value)
|
|
72
|
+
value = value.sub(/^"/, '')
|
|
73
|
+
value = value.sub(/"$/, '')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
pairs[key.to_sym] = value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
pairs
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def request
|
|
84
|
+
@request ||= begin
|
|
85
|
+
options = SiteInspector.typhoeus_defaults
|
|
86
|
+
options = options.merge(method: :get)
|
|
87
|
+
Typhoeus::Request.new(url, options)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def url
|
|
92
|
+
url = Addressable::URI.parse(STATUS_ENDPOINT)
|
|
93
|
+
url.query_values = { domain: endpoint.host.to_s }
|
|
94
|
+
url
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def preload_list
|
|
98
|
+
@preload_list ||= begin
|
|
99
|
+
SiteInspector.hydra.queue(request)
|
|
100
|
+
SiteInspector.hydra.run
|
|
101
|
+
|
|
102
|
+
response = request.response
|
|
103
|
+
if response.success?
|
|
104
|
+
JSON.parse(response.body)
|
|
105
|
+
else
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SiteInspector
|
|
4
|
+
class Endpoint
|
|
5
|
+
class Wappalyzer < Check
|
|
6
|
+
class WappalyzerError < RuntimeError; end
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def wappalyzer?
|
|
10
|
+
return @wappalyzer_detected if defined? @wappalyzer_detected
|
|
11
|
+
|
|
12
|
+
@wappalyzer_detected = !!wappalyzer.detect
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def enabled?
|
|
16
|
+
@@enabled && wappalyzer?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def wappalyzer
|
|
20
|
+
@wappalyzer ||= begin
|
|
21
|
+
path = ['*', './bin', './node_modules/.bin'].join(File::PATH_SEPARATOR)
|
|
22
|
+
Cliver::Dependency.new('wappalyzer', path:)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run_command(args)
|
|
27
|
+
Open3.capture2e(wappalyzer.detect, *args)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
return {} unless endpoint.up?
|
|
33
|
+
return {} if endpoint.redirect?
|
|
34
|
+
|
|
35
|
+
@to_h ||= begin
|
|
36
|
+
technologies = {}
|
|
37
|
+
|
|
38
|
+
data['technologies']&.each do |t|
|
|
39
|
+
category = t['categories'].first
|
|
40
|
+
category = category ? category['name'] : 'other'
|
|
41
|
+
category = category.downcase.tr(' ', '_').to_sym
|
|
42
|
+
technologies[category] ||= []
|
|
43
|
+
technologies[category].push t['name']
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
technologies
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def data
|
|
53
|
+
@data ||= begin
|
|
54
|
+
args = [endpoint.uri.to_s]
|
|
55
|
+
output, _status = self.class.run_command(args)
|
|
56
|
+
JSON.parse(output)
|
|
57
|
+
rescue Timeout::Error, IOError
|
|
58
|
+
{ technologies: {
|
|
59
|
+
categories: ['timeout'],
|
|
60
|
+
name: "https://www.wappalyzer.com/lookup/#{endpoint.host}"
|
|
61
|
+
} }
|
|
62
|
+
rescue JSON::ParserError
|
|
63
|
+
raise WappalyzerError, "Command `wappalyzer #{args.join(' ')}` failed: #{output}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
class SiteInspector
|
|
6
|
+
class Endpoint
|
|
7
|
+
class WellKnown < Check
|
|
8
|
+
BASE_PATH = './.well-known/'
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def well_knowns
|
|
12
|
+
data = File.expand_path '../../data/well-known.yml', File.dirname(__FILE__)
|
|
13
|
+
wks = YAML.load_file(data)
|
|
14
|
+
wks.reject { |wk| wk.include?('deprecated') }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def keys
|
|
18
|
+
well_knowns.map { |wk| key_for(wk) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def key_paths
|
|
22
|
+
@key_paths ||= begin
|
|
23
|
+
key_paths = well_knowns.map do |wk|
|
|
24
|
+
[key_for(wk), path_for(wk)]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
key_paths.to_h
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def path_for(wk)
|
|
32
|
+
return unless well_knowns.include?(wk)
|
|
33
|
+
|
|
34
|
+
"#{BASE_PATH}#{wk}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def key_for(well_known)
|
|
38
|
+
well_known.gsub(/(\.|-)/, '_').to_sym
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def exists?(path)
|
|
43
|
+
endpoint.content.path_exists?(path)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def uri_for(key)
|
|
47
|
+
path = self.class.key_paths[key]
|
|
48
|
+
endpoint.join(path) if path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
return {} unless endpoint.up?
|
|
53
|
+
return {} if endpoint.redirect?
|
|
54
|
+
return @hash if defined?(@hash)
|
|
55
|
+
|
|
56
|
+
prefetch
|
|
57
|
+
@hash = {}
|
|
58
|
+
|
|
59
|
+
self.class.keys.each do |key|
|
|
60
|
+
@hash[key] = public_send(key)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
@hash
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def method_missing(method_sym, *arguments, &)
|
|
67
|
+
if respond_to_missing?(method_sym)
|
|
68
|
+
exists?(self.class.key_paths[method_sym])
|
|
69
|
+
else
|
|
70
|
+
super
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def respond_to_missing?(method_sym, include_private = false)
|
|
75
|
+
if self.class.keys.include?(method_sym)
|
|
76
|
+
true
|
|
77
|
+
else
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def prefetch
|
|
85
|
+
options = SiteInspector.typhoeus_defaults.merge(followlocation: true)
|
|
86
|
+
self.class.key_paths.each do |_key, path|
|
|
87
|
+
uri = endpoint.join(path)
|
|
88
|
+
request = Typhoeus::Request.new(uri, options)
|
|
89
|
+
SiteInspector.hydra.queue(request)
|
|
90
|
+
end
|
|
91
|
+
SiteInspector.hydra.run
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class SiteInspector
|
|
4
|
+
class Endpoint
|
|
5
|
+
class Whois < Check
|
|
6
|
+
def domain
|
|
7
|
+
return @domain if defined? @domain
|
|
8
|
+
|
|
9
|
+
@domain = begin
|
|
10
|
+
whois.lookup host
|
|
11
|
+
rescue Timeout::Error
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ip
|
|
17
|
+
return @ip if defined? @ip
|
|
18
|
+
|
|
19
|
+
@ip = begin
|
|
20
|
+
whois.lookup ip_address if ip_address
|
|
21
|
+
rescue Timeout::Error
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_h
|
|
27
|
+
{
|
|
28
|
+
domain: record_to_h(domain),
|
|
29
|
+
ip: record_to_h(ip)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def record_to_h(record)
|
|
36
|
+
return unless record
|
|
37
|
+
|
|
38
|
+
record.content.scan(/^\s*(.*?):\s*(.*?)\r?\n/).to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ip_address
|
|
42
|
+
return @ip_address if defined? @ip_address
|
|
43
|
+
|
|
44
|
+
@ip_address = begin
|
|
45
|
+
Resolv.getaddress host
|
|
46
|
+
rescue Resolv::ResolvError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def whois
|
|
52
|
+
@whois ||= ::Whois::Client.new
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|