site-inspector 3.2.0 → 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 +4 -4
- data/.github/workflows/ci.yml +30 -0
- data/.rubocop.yml +1 -5
- data/.rubocop_todo.yml +185 -79
- data/.ruby-version +1 -1
- data/CHANGELOG.md +71 -0
- data/Gemfile +0 -2
- data/README.md +8 -14
- data/bin/site-inspector +13 -13
- data/lib/cliver/dependency_ext.rb +2 -2
- data/lib/data/well-known.yml +51 -0
- data/lib/site-inspector/domain.rb +27 -15
- data/lib/site-inspector/domain_parser.rb +11 -0
- data/lib/site-inspector/{checks → endpoint}/accessibility.rb +6 -4
- data/lib/site-inspector/{checks → endpoint}/check.rb +5 -1
- data/lib/site-inspector/endpoint/content.rb +151 -0
- data/lib/site-inspector/{checks → endpoint}/cookies.rb +13 -5
- data/lib/site-inspector/{checks → endpoint}/dns.rb +42 -18
- data/lib/site-inspector/endpoint/headers.rb +55 -0
- data/lib/site-inspector/{checks → endpoint}/hsts.rb +36 -5
- 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 +60 -23
- data/lib/site-inspector/formatter.rb +118 -0
- data/lib/site-inspector/version.rb +1 -1
- data/lib/site-inspector.rb +34 -30
- data/package-lock.json +670 -23
- 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 +1 -1
- data/script/release +11 -4
- data/script/update-well-known +18 -0
- data/site-inspector.gemspec +16 -9
- data/spec/cliver/dependency_ext_spec.rb +21 -0
- data/spec/{site_inspector_domain_spec.rb → site_inspector/domain_spec.rb} +71 -9
- data/spec/{checks/site_inspector_endpoint_accessibility_spec.rb → site_inspector/endpoint/accessibility_spec.rb} +9 -7
- data/spec/{checks/site_inspector_endpoint_content_spec.rb → site_inspector/endpoint/content_spec.rb} +14 -4
- data/spec/{checks/site_inspector_endpoint_cookies_spec.rb → site_inspector/endpoint/cookies_spec.rb} +27 -14
- data/spec/{checks/site_inspector_endpoint_dns_spec.rb → site_inspector/endpoint/dns_spec.rb} +87 -9
- data/spec/{checks/site_inspector_endpoint_headers_spec.rb → site_inspector/endpoint/headers_spec.rb} +0 -7
- data/spec/{checks/site_inspector_endpoint_hsts_spec.rb → site_inspector/endpoint/hsts_spec.rb} +8 -8
- data/spec/site_inspector/endpoint/wappalyzer_spec.rb +20 -0
- data/spec/site_inspector/endpoint/well_known_spec.rb +12 -0
- data/spec/{checks/site_inspector_endpoint_whois_spec.rb → site_inspector/endpoint/whois_spec.rb} +2 -1
- data/spec/{site_inspector_endpoint_spec.rb → site_inspector/endpoint_spec.rb} +59 -3
- data/spec/site_inspector_spec.rb +58 -11
- data/spec/spec_helper.rb +9 -2
- metadata +100 -92
- data/.travis.yml +0 -9
- data/lib/site-inspector/cache.rb +0 -17
- data/lib/site-inspector/checks/content.rb +0 -85
- data/lib/site-inspector/checks/headers.rb +0 -68
- data/lib/site-inspector/checks/sniffer.rb +0 -67
- data/lib/site-inspector/checks/wappalyzer.rb +0 -62
- data/lib/site-inspector/checks/whois.rb +0 -36
- data/lib/site-inspector/disk_cache.rb +0 -42
- data/lib/site-inspector/rails_cache.rb +0 -13
- data/spec/checks/site_inspector_endpoint_sniffer_spec.rb +0 -150
- data/spec/checks/site_inspector_endpoint_wappalyzer_spec.rb +0 -34
- data/spec/site_inspector_cache_spec.rb +0 -15
- data/spec/site_inspector_disk_cache_spec.rb +0 -39
- /data/lib/site-inspector/{checks → endpoint}/https.rb +0 -0
- /data/spec/{checks/site_inspector_endpoint_check_spec.rb → site_inspector/endpoint/check_spec.rb} +0 -0
- /data/spec/{checks/site_inspector_endpoint_https_spec.rb → site_inspector/endpoint/https_spec.rb} +0 -0
data/package.json
CHANGED
data/renovate.json
ADDED
data/script/benchmark
ADDED
data/script/cibuild
CHANGED
data/script/console
CHANGED
data/script/pa11y-version
CHANGED
data/script/release
CHANGED
|
@@ -12,10 +12,17 @@ cd $(dirname "$0")/..
|
|
|
12
12
|
rm -rf site-inspector-*.gem
|
|
13
13
|
gem build -q site-inspector.gemspec
|
|
14
14
|
|
|
15
|
-
# Make sure we're on the
|
|
15
|
+
# Make sure we're on the default branch.
|
|
16
16
|
|
|
17
|
-
(git
|
|
18
|
-
|
|
17
|
+
default_branch=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
|
|
18
|
+
[ -n "$default_branch" ] || default_branch=$(git remote show origin | sed -n 's/.*HEAD branch: //p')
|
|
19
|
+
[ -n "$default_branch" ] || {
|
|
20
|
+
echo "Couldn't determine the default branch."
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[ "$(git rev-parse --abbrev-ref HEAD)" = "$default_branch" ] || {
|
|
25
|
+
echo "Only release from the ${default_branch} branch."
|
|
19
26
|
exit 1
|
|
20
27
|
}
|
|
21
28
|
|
|
@@ -35,4 +42,4 @@ git fetch -t origin
|
|
|
35
42
|
# Tag it and bag it.
|
|
36
43
|
|
|
37
44
|
gem push site-inspector-*.gem && git tag "$tag" &&
|
|
38
|
-
git push origin
|
|
45
|
+
git push origin "$default_branch" && git push origin "$tag"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'nokogiri'
|
|
5
|
+
require 'typhoeus'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
|
|
8
|
+
path = './lib/data/well-known.yml'
|
|
9
|
+
source = 'https://www.iana.org/assignments/well-known-uris/well-known-uris.xml'
|
|
10
|
+
response = Typhoeus.get(source)
|
|
11
|
+
xml = Nokogiri::XML(response.body)
|
|
12
|
+
|
|
13
|
+
paths = xml.css('registry record').map do |record|
|
|
14
|
+
record.css('value').children.to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
yaml = YAML.dump paths
|
|
18
|
+
File.write(path, yaml)
|
data/site-inspector.gemspec
CHANGED
|
@@ -5,7 +5,7 @@ require File.expand_path './lib/site-inspector/version', File.dirname(__FILE__)
|
|
|
5
5
|
Gem::Specification.new do |s|
|
|
6
6
|
s.name = 'site-inspector'
|
|
7
7
|
s.version = SiteInspector::VERSION
|
|
8
|
-
s.summary =
|
|
8
|
+
s.summary = "Ruby gem and CLI that checks a domain's HTTPS, HSTS, DNS, headers, cookies, WHOIS, accessibility, and tech stack"
|
|
9
9
|
s.description = "Returns information about a domain's technology and capabilities"
|
|
10
10
|
s.authors = 'Ben Balter'
|
|
11
11
|
s.email = 'ben@balter.com'
|
|
@@ -14,20 +14,22 @@ Gem::Specification.new do |s|
|
|
|
14
14
|
|
|
15
15
|
s.files = `git ls-files -z`.split("\x0")
|
|
16
16
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
17
|
-
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
|
18
17
|
s.require_paths = ['lib']
|
|
19
18
|
|
|
19
|
+
s.required_ruby_version = '>= 3.2'
|
|
20
|
+
|
|
21
|
+
s.add_dependency('activesupport', '>= 7.0')
|
|
20
22
|
s.add_dependency('cliver', '~> 0.0')
|
|
21
23
|
s.add_dependency('colorator', '~> 1.1')
|
|
24
|
+
s.add_dependency('csv', '~> 3.0')
|
|
22
25
|
s.add_dependency('dnsruby', '~> 1.0')
|
|
23
|
-
s.add_dependency('
|
|
24
|
-
s.add_dependency('
|
|
26
|
+
s.add_dependency('gman', '>= 7.0.4', '< 9')
|
|
27
|
+
s.add_dependency('http-cookie', '~> 1.0')
|
|
25
28
|
s.add_dependency('mercenary', '~> 0.0')
|
|
26
|
-
s.add_dependency('
|
|
27
|
-
s.add_dependency('
|
|
29
|
+
s.add_dependency('naughty_or_nice', '~> 2.0')
|
|
30
|
+
s.add_dependency('nokogiri', '~> 1.10')
|
|
28
31
|
s.add_dependency('parallel', '~> 1.0')
|
|
29
|
-
s.add_dependency('public_suffix', '
|
|
30
|
-
s.add_dependency('sniffles', '~> 0.0')
|
|
32
|
+
s.add_dependency('public_suffix', '>= 4', '< 6')
|
|
31
33
|
s.add_dependency('typhoeus', '~> 1.0')
|
|
32
34
|
s.add_dependency('urlscan', '~> 0.6')
|
|
33
35
|
s.add_dependency('whois', '~> 5.0')
|
|
@@ -37,6 +39,11 @@ Gem::Specification.new do |s|
|
|
|
37
39
|
s.add_development_dependency('rspec', '~> 3.0')
|
|
38
40
|
s.add_development_dependency('rubocop', '~> 1.0')
|
|
39
41
|
s.add_development_dependency('rubocop-performance', '~> 1.5')
|
|
40
|
-
s.add_development_dependency('rubocop-rspec', '~>
|
|
42
|
+
s.add_development_dependency('rubocop-rspec', '~> 3.0')
|
|
41
43
|
s.add_development_dependency('webmock', '~> 3.0')
|
|
44
|
+
s.metadata['rubygems_mfa_required'] = 'true'
|
|
45
|
+
s.metadata['homepage_uri'] = 'https://github.com/benbalter/site-inspector'
|
|
46
|
+
s.metadata['source_code_uri'] = 'https://github.com/benbalter/site-inspector'
|
|
47
|
+
s.metadata['bug_tracker_uri'] = 'https://github.com/benbalter/site-inspector/issues'
|
|
48
|
+
s.metadata['changelog_uri'] = 'https://github.com/benbalter/site-inspector/releases'
|
|
42
49
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Cliver::Dependency do
|
|
6
|
+
subject(:dependency) { described_class.new('foo') }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
allow(dependency).to receive_messages(path: '/usr/bin/foo', installed_versions: [['/usr/bin/foo', '1.2.3']])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'returns the version of the detected executable' do
|
|
13
|
+
expect(dependency.version).to eql('1.2.3')
|
|
14
|
+
expect(dependency.major_version).to eql('1')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'memoizes the version' do
|
|
18
|
+
2.times { dependency.version }
|
|
19
|
+
expect(dependency).to have_received(:installed_versions).once
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -8,37 +8,54 @@ describe SiteInspector::Domain do
|
|
|
8
8
|
context 'domain parsing' do
|
|
9
9
|
it 'downcases the domain' do
|
|
10
10
|
domain = described_class.new('EXAMPLE.com')
|
|
11
|
-
expect(domain.host).to eql('example.com')
|
|
11
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
it 'strips http from the domain' do
|
|
15
15
|
domain = described_class.new('http://example.com')
|
|
16
|
-
expect(domain.host).to eql('example.com')
|
|
16
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
it 'strips https from the domain' do
|
|
20
20
|
domain = described_class.new('https://example.com')
|
|
21
|
-
expect(domain.host).to eql('example.com')
|
|
21
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
it 'strips www from the domain' do
|
|
25
25
|
domain = described_class.new('www.example.com')
|
|
26
|
-
expect(domain.host).to eql('example.com')
|
|
26
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'strips a leading www from a deeper subdomain' do
|
|
30
|
+
domain = described_class.new('www.foo.example.com')
|
|
31
|
+
expect(domain.host.to_s).to eql('foo.example.com')
|
|
32
|
+
expect(domain.host.trd).to eql('foo')
|
|
33
|
+
expect(domain.host).to be_a(PublicSuffix::Domain)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'strips www and leaves no subdomain' do
|
|
37
|
+
domain = described_class.new('www.example.com')
|
|
38
|
+
expect(domain.host.trd).to be_nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'does not strip www from the middle of a subdomain' do
|
|
42
|
+
domain = described_class.new('foo.www.example.com')
|
|
43
|
+
expect(domain.host.to_s).to eql('foo.www.example.com')
|
|
27
44
|
end
|
|
28
45
|
|
|
29
46
|
it 'strips http://www from the domain' do
|
|
30
47
|
domain = described_class.new('http://www.example.com')
|
|
31
|
-
expect(domain.host).to eql('example.com')
|
|
48
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
it 'strips paths from the domain' do
|
|
35
52
|
domain = described_class.new('http://www.example.com/foo')
|
|
36
|
-
expect(domain.host).to eql('example.com')
|
|
53
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
37
54
|
end
|
|
38
55
|
|
|
39
56
|
it 'strips trailing slashes from the domain' do
|
|
40
57
|
domain = described_class.new('http://www.example.com/')
|
|
41
|
-
expect(domain.host).to eql('example.com')
|
|
58
|
+
expect(domain.host.to_s).to eql('example.com')
|
|
42
59
|
end
|
|
43
60
|
end
|
|
44
61
|
|
|
@@ -186,8 +203,53 @@ describe SiteInspector::Domain do
|
|
|
186
203
|
expect(subject.enforces_https?).to be(false)
|
|
187
204
|
end
|
|
188
205
|
|
|
189
|
-
it 'detects when a domain downgrades to http' do
|
|
190
|
-
|
|
206
|
+
it 'detects when a domain downgrades to http from the canonical https endpoint' do
|
|
207
|
+
stub_request(:head, 'https://example.com/')
|
|
208
|
+
.to_return(status: 301, headers: { location: 'http://example.com' })
|
|
209
|
+
stub_request(:head, 'https://www.example.com/').to_return(status: 500)
|
|
210
|
+
stub_request(:head, 'http://example.com/').to_return(status: 200)
|
|
211
|
+
stub_request(:head, 'http://www.example.com/').to_return(status: 500)
|
|
212
|
+
allow(subject.endpoints[0].https).to receive(:valid?).and_return(true)
|
|
213
|
+
|
|
214
|
+
expect(subject.downgrades_https?).to be(true)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'detects when a domain downgrades to http even when canonical is http (nytimes.com case)' do
|
|
218
|
+
# This is the key test case from the issue:
|
|
219
|
+
# https://nytimes.com redirects to http://www.nytimes.com (canonical)
|
|
220
|
+
# The canonical endpoint is HTTP, but HTTPS is supported and downgrades to HTTP
|
|
221
|
+
stub_request(:head, 'https://example.com/')
|
|
222
|
+
.to_return(status: 301, headers: { location: 'http://www.example.com' })
|
|
223
|
+
stub_request(:head, 'https://www.example.com/')
|
|
224
|
+
.to_return(status: 301, headers: { location: 'http://www.example.com' })
|
|
225
|
+
stub_request(:head, 'http://example.com/')
|
|
226
|
+
.to_return(status: 301, headers: { location: 'http://www.example.com' })
|
|
227
|
+
stub_request(:head, 'http://www.example.com/').to_return(status: 200)
|
|
228
|
+
allow(subject.endpoints[0].https).to receive(:valid?).and_return(true)
|
|
229
|
+
allow(subject.endpoints[1].https).to receive(:valid?).and_return(true)
|
|
230
|
+
|
|
231
|
+
expect(subject.downgrades_https?).to be(true)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it 'does not consider a domain as downgrading when https is not supported' do
|
|
235
|
+
stub_request(:head, 'https://example.com/').to_return(status: 500)
|
|
236
|
+
stub_request(:head, 'https://www.example.com/').to_return(status: 500)
|
|
237
|
+
stub_request(:head, 'http://example.com/').to_return(status: 200)
|
|
238
|
+
stub_request(:head, 'http://www.example.com/').to_return(status: 200)
|
|
239
|
+
|
|
240
|
+
expect(subject.downgrades_https?).to be(false)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'does not consider a domain as downgrading when https redirects to https' do
|
|
244
|
+
stub_request(:head, 'https://example.com/')
|
|
245
|
+
.to_return(status: 301, headers: { location: 'https://www.example.com' })
|
|
246
|
+
stub_request(:head, 'https://www.example.com/').to_return(status: 200)
|
|
247
|
+
stub_request(:head, 'http://example.com/').to_return(status: 500)
|
|
248
|
+
stub_request(:head, 'http://www.example.com/').to_return(status: 500)
|
|
249
|
+
allow(subject.endpoints[0].https).to receive(:valid?).and_return(true)
|
|
250
|
+
allow(subject.endpoints[1].https).to receive(:valid?).and_return(true)
|
|
251
|
+
|
|
252
|
+
expect(subject.downgrades_https?).to be(false)
|
|
191
253
|
end
|
|
192
254
|
|
|
193
255
|
it 'detects when a domain enforces https' do
|
|
@@ -43,32 +43,33 @@ describe SiteInspector::Endpoint::Accessibility do
|
|
|
43
43
|
expect { subject.standard = :foo }.to raise_error(ArgumentError)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
context 'with pa11y installed' do
|
|
47
|
-
before do
|
|
48
|
-
stub_request(:head, 'http://example.com/').to_return(status: 200)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
46
|
context "with pa11y stub'd" do
|
|
53
47
|
before do
|
|
54
48
|
output = '[{"code":"Section508.L.NoContentAnchor","context":"<a href=\"foo\"></a>","message":"Anchor element found with a valid href attribute, but no link content has been supplied.","selector":"html > body > a","type":"error","typeCode":1}]'
|
|
55
49
|
allow(subject).to receive(:run_command) { [output, 2] }
|
|
56
50
|
end
|
|
57
51
|
|
|
52
|
+
before do
|
|
53
|
+
stub_request(:head, 'http://example.com/').to_return(status: 200)
|
|
54
|
+
end
|
|
55
|
+
|
|
58
56
|
it 'knows if a site is valid' do
|
|
59
|
-
with_env 'SKIP_PA11Y_CHECK', 'true' do
|
|
57
|
+
with_env 'SKIP_PA11Y_CHECK', 'true' do \
|
|
58
|
+
skip
|
|
60
59
|
expect(subject.valid?).to be(false)
|
|
61
60
|
end
|
|
62
61
|
end
|
|
63
62
|
|
|
64
63
|
it 'counts the errors' do
|
|
65
64
|
with_env 'SKIP_PA11Y_CHECK', 'true' do
|
|
65
|
+
skip
|
|
66
66
|
expect(subject.errors).to be(1)
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
it 'runs the check' do
|
|
71
71
|
with_env 'SKIP_PA11Y_CHECK', 'true' do
|
|
72
|
+
skip
|
|
72
73
|
expect(subject.check[:valid]).to be(false)
|
|
73
74
|
expect(subject.check[:results].first['code']).to eql('WCAG2A.Principle3.Guideline3_1.3_1_1.H57.2')
|
|
74
75
|
end
|
|
@@ -76,6 +77,7 @@ describe SiteInspector::Endpoint::Accessibility do
|
|
|
76
77
|
|
|
77
78
|
it 'runs a named check' do
|
|
78
79
|
with_env 'SKIP_PA11Y_CHECK', 'true' do
|
|
80
|
+
skip
|
|
79
81
|
expect(subject.check[:valid]).to be(false)
|
|
80
82
|
expect(subject.check[:results].first['code']).to eql('WCAG2A.Principle3.Guideline3_1.3_1_1.H57.2')
|
|
81
83
|
end
|
data/spec/{checks/site_inspector_endpoint_content_spec.rb → site_inspector/endpoint/content_spec.rb}
RENAMED
|
@@ -17,7 +17,7 @@ describe SiteInspector::Endpoint::Content do
|
|
|
17
17
|
BODY
|
|
18
18
|
|
|
19
19
|
stub_request(:get, 'http://example.com/')
|
|
20
|
-
.to_return(status: 200, body:
|
|
20
|
+
.to_return(status: 200, body:)
|
|
21
21
|
stub_request(:head, 'http://example.com/')
|
|
22
22
|
.to_return(status: 200)
|
|
23
23
|
endpoint = SiteInspector::Endpoint.new('http://example.com')
|
|
@@ -85,6 +85,16 @@ describe SiteInspector::Endpoint::Content do
|
|
|
85
85
|
expect(subject.humans_txt?).to be(true)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
it 'finds security.txt under .well-known' do
|
|
89
|
+
stub_request(:head, %r{http://example.com/[a-z0-9]{32}}i).to_return(status: 404)
|
|
90
|
+
stub_request(:head, 'http://example.com/security.txt').to_return(status: 404)
|
|
91
|
+
stub = stub_request(:head, 'http://example.com/.well-known/security.txt')
|
|
92
|
+
.to_return(status: 200)
|
|
93
|
+
|
|
94
|
+
expect(subject.security_txt?).to be(true)
|
|
95
|
+
expect(stub).to have_been_requested
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
it 'returns the generator' do
|
|
89
99
|
expect(subject.generator).to eql('Jekyll v3.8.5')
|
|
90
100
|
end
|
|
@@ -103,15 +113,15 @@ describe SiteInspector::Endpoint::Content do
|
|
|
103
113
|
end
|
|
104
114
|
|
|
105
115
|
it 'generates a random path' do
|
|
106
|
-
path = subject.send(:
|
|
116
|
+
path = subject.send(:random_paths).first
|
|
107
117
|
expect(path).to match(/[a-z0-9]{32}/i)
|
|
108
|
-
expect(subject.send(:
|
|
118
|
+
expect(subject.send(:random_paths).first).to eql(path)
|
|
109
119
|
end
|
|
110
120
|
|
|
111
121
|
it "doesn't say something exists when there are no 404s" do
|
|
112
122
|
stub_request(:head, %r{http://example.com/[a-z0-9]{32}}i).to_return(status: 200)
|
|
113
123
|
stub_request(:head, 'http://example.com/humans.txt').to_return(status: 200)
|
|
114
|
-
expect(subject.humans_txt?).to
|
|
124
|
+
expect(subject.humans_txt?).to be_nil
|
|
115
125
|
end
|
|
116
126
|
end
|
|
117
127
|
end
|
data/spec/{checks/site_inspector_endpoint_cookies_spec.rb → site_inspector/endpoint/cookies_spec.rb}
RENAMED
|
@@ -13,26 +13,16 @@ describe SiteInspector::Endpoint::Cookies do
|
|
|
13
13
|
|
|
14
14
|
it 'knows when there are no cookies' do
|
|
15
15
|
expect(subject.cookies?).to be(false)
|
|
16
|
-
expect(subject.all).to
|
|
16
|
+
expect(subject.all).to be_nil
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
context 'with cookies' do
|
|
21
21
|
subject do
|
|
22
22
|
cookies = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'domain' => 'example.com',
|
|
27
|
-
'path' => '/'
|
|
28
|
-
),
|
|
29
|
-
CGI::Cookie.new(
|
|
30
|
-
'name' => 'foo2',
|
|
31
|
-
'value' => 'bar2',
|
|
32
|
-
'domain' => 'example.com',
|
|
33
|
-
'path' => '/'
|
|
34
|
-
)
|
|
35
|
-
].map(&:to_s)
|
|
23
|
+
'foo=bar; domain=example.com; path=/',
|
|
24
|
+
'foo2=bar2; domain=example.com; path=/'
|
|
25
|
+
]
|
|
36
26
|
|
|
37
27
|
stub_request(:head, 'http://example.com/')
|
|
38
28
|
.to_return(status: 200, body: '', headers: { 'set-cookie' => cookies })
|
|
@@ -69,5 +59,28 @@ describe SiteInspector::Endpoint::Cookies do
|
|
|
69
59
|
it 'knows cookies are secure' do
|
|
70
60
|
expect(subject.secure?).to be(true)
|
|
71
61
|
end
|
|
62
|
+
|
|
63
|
+
it 'parses cookie attributes' do
|
|
64
|
+
expect(subject['foo'].secure?).to be(true)
|
|
65
|
+
expect(subject['foo'].httponly?).to be(true)
|
|
66
|
+
expect(subject['foo2'].secure?).to be(false)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context 'with Secure and HttpOnly on different cookies' do
|
|
71
|
+
subject do
|
|
72
|
+
cookies = [
|
|
73
|
+
'foo=bar; domain=example.com; path=/; secure',
|
|
74
|
+
'foo2=bar2; domain=example.com; path=/; HttpOnly'
|
|
75
|
+
]
|
|
76
|
+
stub_request(:head, 'http://example.com/')
|
|
77
|
+
.to_return(status: 200, body: '', headers: { 'set-cookie' => cookies })
|
|
78
|
+
endpoint = SiteInspector::Endpoint.new('http://example.com')
|
|
79
|
+
described_class.new(endpoint)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "doesn't count split flags as secure" do
|
|
83
|
+
expect(subject.secure?).to be(false)
|
|
84
|
+
end
|
|
72
85
|
end
|
|
73
86
|
end
|
data/spec/{checks/site_inspector_endpoint_dns_spec.rb → site_inspector/endpoint/dns_spec.rb}
RENAMED
|
@@ -14,8 +14,66 @@ describe SiteInspector::Endpoint::Dns do
|
|
|
14
14
|
expect(described_class.resolver.class).to eql(Dnsruby::Resolver)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
context 'with a stubbed resolver' do
|
|
18
|
+
let(:resolver) { described_class.resolver }
|
|
19
|
+
let(:answers) do
|
|
20
|
+
{
|
|
21
|
+
%w[github.com A] => [Dnsruby::RR.create(type: 'A', name: 'github.com', address: '140.82.112.3')],
|
|
22
|
+
%w[github.com MX] => [Dnsruby::RR.create(type: 'MX', name: 'github.com', exchange: 'aspmx.l.google.com', preference: 1)],
|
|
23
|
+
%w[3.112.82.140.in-addr.arpa PTR] => [
|
|
24
|
+
Dnsruby::RR.create(type: 'PTR', name: '3.112.82.140.in-addr.arpa', domainname: 'lb-140-82-112-3-iad.github.com')
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
allow(resolver).to receive(:query) do |name, type|
|
|
31
|
+
name = Dnsruby::Message.new(name, type).question.first.qname.to_s
|
|
32
|
+
instance_double(Dnsruby::Message, answer: answers.fetch([name, type.to_s], []))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'queries explicit record types instead of ANY' do
|
|
37
|
+
subject.records
|
|
38
|
+
described_class::RECORD_TYPES.each do |type|
|
|
39
|
+
expect(resolver).to have_received(:query).with('github.com', type)
|
|
40
|
+
end
|
|
41
|
+
expect(resolver).not_to have_received(:query).with(anything, 'ANY')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'collects records across types' do
|
|
45
|
+
expect(subject.records.map { |r| r.type.to_s }).to contain_exactly('A', 'MX')
|
|
46
|
+
expect(subject.google_apps?).to be(true)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'resolves the IP from the A record' do
|
|
50
|
+
expect(subject.ip).to eql('140.82.112.3')
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'falls back to the AAAA record' do
|
|
54
|
+
answers.delete(%w[github.com A])
|
|
55
|
+
answers[%w[github.com AAAA]] = [Dnsruby::RR.create(type: 'AAAA', name: 'github.com', address: '2001:db8::1')]
|
|
56
|
+
expect(subject.ip).to eql('2001:DB8::1')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'resolves the hostname with a PTR query through the same resolver' do
|
|
60
|
+
expect(subject.hostname.to_s).to eql('lb-140-82-112-3-iad.github.com')
|
|
61
|
+
expect(resolver).to have_received(:query).with('140.82.112.3', 'PTR')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'returns no hostname without an IP' do
|
|
65
|
+
answers.delete(%w[github.com A])
|
|
66
|
+
expect(subject.hostname).to be_nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'returns no records when the resolver errors' do
|
|
70
|
+
allow(resolver).to receive(:query).and_raise(Dnsruby::Refused, 'refused')
|
|
71
|
+
expect(subject.records).to eql([])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# NOTE: these tests make external calls; run them with LIVE=1
|
|
76
|
+
context 'live tests', :live do
|
|
19
77
|
it 'runs the query' do
|
|
20
78
|
expect(subject.query).not_to be_empty
|
|
21
79
|
end
|
|
@@ -53,12 +111,12 @@ describe SiteInspector::Endpoint::Dns do
|
|
|
53
111
|
|
|
54
112
|
# via https://github.com/alexdalitz/dnsruby/blob/master/test/tc_dnskey.rb
|
|
55
113
|
input = 'example.com. 86400 IN DNSKEY 256 3 5 ( AQPSKmynfzW4kyBv015MUG2DeIQ3' \
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
114
|
+
'Cbl+BBZH4b/0PY1kxkmvHjcZc8no' \
|
|
115
|
+
'kfzj31GajIQKY+5CptLr3buXA10h' \
|
|
116
|
+
'WqTkF7H6RfoRqXQeogmMHfpftf6z' \
|
|
117
|
+
'Mv1LyBUgia7za6ZEzOJBOztyvhjL' \
|
|
118
|
+
'742iU/TpPSEDhm2SNKLijfUppn1U' \
|
|
119
|
+
'aNvv4w== )'
|
|
62
120
|
|
|
63
121
|
record = Dnsruby::RR.create input
|
|
64
122
|
allow(subject).to receive(:records) { [record] }
|
|
@@ -125,7 +183,7 @@ describe SiteInspector::Endpoint::Dns do
|
|
|
125
183
|
|
|
126
184
|
it 'builds that path to a data file' do
|
|
127
185
|
path = subject.send(:data_path, 'foo')
|
|
128
|
-
expected = File.expand_path '
|
|
186
|
+
expected = File.expand_path '../../../lib/data/foo.yml', File.dirname(__FILE__)
|
|
129
187
|
expect(path).to eql(expected)
|
|
130
188
|
end
|
|
131
189
|
|
|
@@ -177,6 +235,26 @@ describe SiteInspector::Endpoint::Dns do
|
|
|
177
235
|
expect(subject.localhost?).to be(true)
|
|
178
236
|
end
|
|
179
237
|
|
|
238
|
+
it 'treats the whole 127.0.0.0/8 block as loopback' do
|
|
239
|
+
allow(subject).to receive(:ip).and_return('127.0.1.1')
|
|
240
|
+
expect(subject.localhost?).to be(true)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'treats the IPv6 loopback as localhost' do
|
|
244
|
+
allow(subject).to receive(:ip).and_return('::1')
|
|
245
|
+
expect(subject.localhost?).to be(true)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "knows a public address isn't localhost" do
|
|
249
|
+
allow(subject).to receive(:ip).and_return('140.82.112.3')
|
|
250
|
+
expect(subject.localhost?).to be(false)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "knows a missing address isn't localhost" do
|
|
254
|
+
allow(subject).to receive(:ip).and_return(nil)
|
|
255
|
+
expect(subject.localhost?).to be(false)
|
|
256
|
+
end
|
|
257
|
+
|
|
180
258
|
it 'returns a LocalhostError' do
|
|
181
259
|
expect(subject.to_h).to eql(error: SiteInspector::Endpoint::Dns::LocalhostError)
|
|
182
260
|
end
|
data/spec/{checks/site_inspector_endpoint_headers_spec.rb → site_inspector/endpoint/headers_spec.rb}
RENAMED
|
@@ -42,13 +42,6 @@ describe SiteInspector::Endpoint::Headers do
|
|
|
42
42
|
expect(subject.xss_protection?).to be(true)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
it 'checks for clickjack proetection' do
|
|
46
|
-
expect(subject.click_jacking_protection?).to be(false)
|
|
47
|
-
stub_header 'x-frame-options', 'foo'
|
|
48
|
-
expect(subject.click_jacking_protection).to eql('foo')
|
|
49
|
-
expect(subject.click_jacking_protection?).to be(true)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
45
|
it 'checks for CSP' do
|
|
53
46
|
expect(subject.content_security_policy?).to be(false)
|
|
54
47
|
stub_header 'content-security-policy', 'foo'
|
data/spec/{checks/site_inspector_endpoint_hsts_spec.rb → site_inspector/endpoint/hsts_spec.rb}
RENAMED
|
@@ -6,7 +6,7 @@ describe SiteInspector::Endpoint::Hsts do
|
|
|
6
6
|
subject do
|
|
7
7
|
headers = { 'strict-transport-security' => 'max-age=31536000; includeSubDomains;' }
|
|
8
8
|
stub_request(:head, 'http://example.com/')
|
|
9
|
-
.to_return(status: 200, headers:
|
|
9
|
+
.to_return(status: 200, headers:)
|
|
10
10
|
endpoint = SiteInspector::Endpoint.new('http://example.com')
|
|
11
11
|
described_class.new(endpoint)
|
|
12
12
|
end
|
|
@@ -30,8 +30,8 @@ describe SiteInspector::Endpoint::Hsts do
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
it 'parses pairs' do
|
|
33
|
-
expect(subject.send(:pairs).keys).to include(:
|
|
34
|
-
expect(subject.send(:pairs)[:
|
|
33
|
+
expect(subject.send(:pairs).keys).to include(:'max-age')
|
|
34
|
+
expect(subject.send(:pairs)[:'max-age']).to eql('31536000')
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
it 'knows if the header is valid' do
|
|
@@ -63,7 +63,7 @@ describe SiteInspector::Endpoint::Hsts do
|
|
|
63
63
|
it "knows if it's enabled" do
|
|
64
64
|
expect(subject.enabled?).to be(true)
|
|
65
65
|
|
|
66
|
-
allow(subject).to receive(:pairs).and_return(
|
|
66
|
+
allow(subject).to receive(:pairs).and_return('max-age': 0)
|
|
67
67
|
expect(subject.preload?).to be(false)
|
|
68
68
|
|
|
69
69
|
allow(subject).to receive(:pairs).and_return(foo: 'bar')
|
|
@@ -73,19 +73,19 @@ describe SiteInspector::Endpoint::Hsts do
|
|
|
73
73
|
it "knows if it's preload ready" do
|
|
74
74
|
expect(subject.preload_ready?).to be(false)
|
|
75
75
|
|
|
76
|
-
pairs = {
|
|
76
|
+
pairs = { 'max-age': 10_886_401, preload: nil, includesubdomains: nil }
|
|
77
77
|
allow(subject).to receive(:pairs) { pairs }
|
|
78
78
|
expect(subject.preload_ready?).to be(true)
|
|
79
79
|
|
|
80
|
-
pairs = {
|
|
80
|
+
pairs = { 'max-age': 10_886_401, includesubdomains: nil }
|
|
81
81
|
allow(subject).to receive(:pairs) { pairs }
|
|
82
82
|
expect(subject.preload_ready?).to be(false)
|
|
83
83
|
|
|
84
|
-
pairs = {
|
|
84
|
+
pairs = { 'max-age': 10_886_401, preload: nil, includesubdomains: nil }
|
|
85
85
|
allow(subject).to receive(:pairs) { pairs }
|
|
86
86
|
expect(subject.preload_ready?).to be(true)
|
|
87
87
|
|
|
88
|
-
pairs = {
|
|
88
|
+
pairs = { 'max-age': 5, preload: nil, includesubdomains: nil }
|
|
89
89
|
allow(subject).to receive(:pairs) { pairs }
|
|
90
90
|
expect(subject.preload_ready?).to be(false)
|
|
91
91
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe SiteInspector::Endpoint::Wappalyzer do
|
|
6
|
+
subject(:wappalyzer) { described_class.new(endpoint) }
|
|
7
|
+
|
|
8
|
+
let(:domain) { 'http://example.com' }
|
|
9
|
+
let(:endpoint) { SiteInspector::Endpoint.new(domain) }
|
|
10
|
+
|
|
11
|
+
it 'raises a WappalyzerError naming the command when output is not JSON' do
|
|
12
|
+
status = instance_double(Process::Status, exitstatus: 1)
|
|
13
|
+
allow(described_class).to receive(:run_command).and_return(['not json', status])
|
|
14
|
+
|
|
15
|
+
expect { wappalyzer.send(:data) }.to raise_error(
|
|
16
|
+
SiteInspector::Endpoint::Wappalyzer::WappalyzerError,
|
|
17
|
+
%r{Command `wappalyzer http://example.com/` failed: not json}
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|