site-inspector 1.0.2 → 2.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/.gitignore +7 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Guardfile +8 -0
- data/README.md +175 -0
- data/Rakefile +8 -0
- data/bin/site-inspector +48 -21
- data/lib/site-inspector.rb +38 -613
- data/lib/site-inspector/cache.rb +9 -52
- data/lib/site-inspector/checks/check.rb +41 -0
- data/lib/site-inspector/checks/content.rb +67 -0
- data/lib/site-inspector/checks/dns.rb +129 -0
- data/lib/site-inspector/checks/headers.rb +83 -0
- data/lib/site-inspector/checks/hsts.rb +78 -0
- data/lib/site-inspector/checks/https.rb +40 -0
- data/lib/site-inspector/checks/sniffer.rb +42 -0
- data/lib/site-inspector/disk_cache.rb +38 -0
- data/lib/site-inspector/domain.rb +248 -0
- data/lib/site-inspector/endpoint.rb +200 -0
- data/lib/site-inspector/rails_cache.rb +11 -0
- data/lib/site-inspector/version.rb +3 -0
- data/script/bootstrap +1 -0
- data/script/cibuild +7 -0
- data/script/console +1 -0
- data/script/release +38 -0
- data/site-inspector.gemspec +33 -0
- data/spec/checks/site_inspector_endpoint_check_spec.rb +34 -0
- data/spec/checks/site_inspector_endpoint_content_spec.rb +89 -0
- data/spec/checks/site_inspector_endpoint_dns_spec.rb +167 -0
- data/spec/checks/site_inspector_endpoint_headers_spec.rb +74 -0
- data/spec/checks/site_inspector_endpoint_hsts_spec.rb +91 -0
- data/spec/checks/site_inspector_endpoint_https_spec.rb +48 -0
- data/spec/checks/site_inspector_endpoint_sniffer_spec.rb +52 -0
- data/spec/site_inspector_cache_spec.rb +13 -0
- data/spec/site_inspector_disc_cache_spec.rb +31 -0
- data/spec/site_inspector_domain_spec.rb +252 -0
- data/spec/site_inspector_endpoint_spec.rb +224 -0
- data/spec/site_inspector_spec.rb +46 -0
- data/spec/spec_helper.rb +17 -0
- metadata +75 -57
- data/lib/site-inspector/compliance.rb +0 -19
- data/lib/site-inspector/dns.rb +0 -92
- data/lib/site-inspector/headers.rb +0 -59
- data/lib/site-inspector/sniffer.rb +0 -26
data/script/bootstrap
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
bundle install
|
data/script/cibuild
ADDED
data/script/console
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
bundle exec pry -r ./lib/site-inspector.rb
|
data/script/release
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
# Tag and push a release.
|
3
|
+
|
4
|
+
set -e
|
5
|
+
|
6
|
+
# Make sure we're in the project root.
|
7
|
+
|
8
|
+
cd $(dirname "$0")/..
|
9
|
+
|
10
|
+
# Build a new gem archive.
|
11
|
+
|
12
|
+
rm -rf site-inspector-*.gem
|
13
|
+
gem build -q site-inspector.gemspec
|
14
|
+
|
15
|
+
# Make sure we're on the master branch.
|
16
|
+
|
17
|
+
(git branch | grep -q '* master') || {
|
18
|
+
echo "Only release from the master branch."
|
19
|
+
exit 1
|
20
|
+
}
|
21
|
+
|
22
|
+
# Figure out what version we're releasing.
|
23
|
+
|
24
|
+
tag=v`ls site-inspector-*.gem | sed 's/^site-inspector-\(.*\)\.gem$/\1/'`
|
25
|
+
|
26
|
+
# Make sure we haven't released this version before.
|
27
|
+
|
28
|
+
git fetch -t origin
|
29
|
+
|
30
|
+
(git tag -l | grep -q "$tag") && {
|
31
|
+
echo "Whoops, there's already a '${tag}' tag."
|
32
|
+
exit 1
|
33
|
+
}
|
34
|
+
|
35
|
+
# Tag it and bag it.
|
36
|
+
|
37
|
+
gem push site-inspector-*.gem && git tag "$tag" &&
|
38
|
+
git push origin master && git push origin "$tag"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path "./lib/site-inspector/version", File.dirname(__FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
|
5
|
+
s.name = "site-inspector"
|
6
|
+
s.version = SiteInspector::VERSION
|
7
|
+
s.summary = "A Ruby port and v2 of Site Inspector (http://github.com/benbalter/site-inspector)"
|
8
|
+
s.description = "Returns information about a domain's technology and capabilities"
|
9
|
+
s.authors = "Ben Balter"
|
10
|
+
s.email = "ben@balter.com"
|
11
|
+
s.homepage = "https://github.com/benbalter/site-inspector-ruby"
|
12
|
+
s.license = "MIT"
|
13
|
+
|
14
|
+
s.files = `git ls-files -z`.split("\x0")
|
15
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
16
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_dependency("nokogiri", "~> 1.6")
|
20
|
+
s.add_dependency("public_suffix", "~> 1.4")
|
21
|
+
s.add_dependency("gman", "~> 4.1")
|
22
|
+
s.add_dependency("dnsruby", "~> 1.56")
|
23
|
+
s.add_dependency("sniffles", "~> 0.2")
|
24
|
+
s.add_dependency("typhoeus", "~> 0.7")
|
25
|
+
s.add_dependency("oj", "~> 2.11")
|
26
|
+
s.add_dependency("mercenary", "~> 0.3")
|
27
|
+
s.add_dependency("colorator", "~> 0.1")
|
28
|
+
s.add_development_dependency("pry", "~> 0.10")
|
29
|
+
s.add_development_dependency( "rake", "~> 10.4" )
|
30
|
+
s.add_development_dependency( "rspec", "~> 3.2")
|
31
|
+
s.add_development_dependency( "bundler", "~> 1.6" )
|
32
|
+
s.add_development_dependency( "webmock", "~> 1.2" )
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SiteInspector::Endpoint::Check do
|
4
|
+
|
5
|
+
subject do
|
6
|
+
stub_request(:get, "http://example.com/").to_return(:status => 200)
|
7
|
+
endpoint = SiteInspector::Endpoint.new("http://example.com")
|
8
|
+
SiteInspector::Endpoint::Check.new(endpoint)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns the endpoint" do
|
12
|
+
expect(subject.endpoint.class).to eql(SiteInspector::Endpoint)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns the response" do
|
16
|
+
expect(subject.response.class).to eql(Typhoeus::Response)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns the request" do
|
20
|
+
expect(subject.request.class).to eql(Typhoeus::Request)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns the host" do
|
24
|
+
expect(subject.host).to eql("example.com")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns its name" do
|
28
|
+
expect(subject.name).to eql(:check)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns the instance name" do
|
32
|
+
expect(SiteInspector::Endpoint::Check.name).to eql(:check)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SiteInspector::Endpoint::Content do
|
4
|
+
|
5
|
+
subject do
|
6
|
+
body = <<-eos
|
7
|
+
<!DOCTYPE html>
|
8
|
+
<html>
|
9
|
+
<body>
|
10
|
+
<h1>Some page</h1>
|
11
|
+
</body>
|
12
|
+
</html>
|
13
|
+
eos
|
14
|
+
|
15
|
+
stub_request(:get, "http://example.com/").
|
16
|
+
to_return(:status => 200, :body => body )
|
17
|
+
endpoint = SiteInspector::Endpoint.new("http://example.com")
|
18
|
+
SiteInspector::Endpoint::Content.new(endpoint)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns the doc" do
|
22
|
+
expect(subject.document.class).to eql(Nokogiri::HTML::Document)
|
23
|
+
expect(subject.document.css("h1").text).to eql("Some page")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns the body" do
|
27
|
+
expect(subject.body).to match("<h1>Some page</h1>")
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns the doctype" do
|
31
|
+
expect(subject.doctype).to eql("html")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "knows when robots.txt exists" do
|
35
|
+
stub_request(:get, "http://example.com/robots.txt").
|
36
|
+
to_return(:status => 200)
|
37
|
+
expect(subject.robots_txt?).to eql(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "knows when robots.txt doesn't exist" do
|
41
|
+
stub_request(:get, "http://example.com/robots.txt").
|
42
|
+
to_return(:status => 404)
|
43
|
+
expect(subject.robots_txt?).to eql(false)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "knows when sitemap.xml exists" do
|
47
|
+
stub_request(:get, "http://example.com/sitemap.xml").
|
48
|
+
to_return(:status => 200)
|
49
|
+
expect(subject.sitemap_xml?).to eql(true)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "knows when sitemap.xml exists" do
|
53
|
+
stub_request(:get, "http://example.com/sitemap.xml").
|
54
|
+
to_return(:status => 404)
|
55
|
+
expect(subject.sitemap_xml?).to eql(false)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "knows when humans.txt exists" do
|
59
|
+
stub_request(:get, "http://example.com/humans.txt").
|
60
|
+
to_return(:status => 200)
|
61
|
+
expect(subject.humans_txt?).to eql(true)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "knows when humans.txt doesn't exist" do
|
65
|
+
stub_request(:get, "http://example.com/humans.txt").
|
66
|
+
to_return(:status => 200)
|
67
|
+
expect(subject.humans_txt?).to eql(true)
|
68
|
+
end
|
69
|
+
|
70
|
+
context "404s" do
|
71
|
+
it "knows when an endpoint returns a proper 404" do
|
72
|
+
stub_request(:get, /http\:\/\/example.com\/.*/).
|
73
|
+
to_return(:status => 404)
|
74
|
+
expect(subject.proper_404s?).to eql(true)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "knows when an endpoint doesn't return a proper 404" do
|
78
|
+
stub_request(:get, /http\:\/\/example.com\/[a-z0-9]{32}/i).
|
79
|
+
to_return(:status => 200)
|
80
|
+
expect(subject.proper_404s?).to eql(false)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "generates a random path" do
|
84
|
+
path = subject.send(:random_path)
|
85
|
+
expect(path).to match /[a-z0-9]{32}/i
|
86
|
+
expect(subject.send(:random_path)).to eql(path)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dnsruby'
|
3
|
+
|
4
|
+
describe SiteInspector::Endpoint::Dns do
|
5
|
+
|
6
|
+
subject do
|
7
|
+
stub_request(:get, "http://github.com/").to_return(:status => 200)
|
8
|
+
endpoint = SiteInspector::Endpoint.new("http://github.com")
|
9
|
+
SiteInspector::Endpoint::Dns.new(endpoint)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "inits the resolver" do
|
13
|
+
expect(SiteInspector::Endpoint::Dns.resolver.class).to eql(Dnsruby::Resolver)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Note: these tests makes external calls
|
17
|
+
context "live tests" do
|
18
|
+
it "it runs the query" do
|
19
|
+
expect(subject.query).not_to be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
context "resolv" do
|
23
|
+
it "returns the IP" do
|
24
|
+
expect(subject.ip).to include("192.30.252.")
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns the hostname" do
|
28
|
+
expect(subject.hostname.sld).to eql("github")
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "stubbed tests" do
|
35
|
+
|
36
|
+
before do
|
37
|
+
record = Dnsruby::RR.create :type => "A", :address => "1.2.3.4", :name => "test"
|
38
|
+
allow(subject).to receive(:records) { [record] }
|
39
|
+
allow(subject).to receive(:query) { [] }
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns the records" do
|
43
|
+
expect(subject.records.count).to eql(1)
|
44
|
+
expect(subject.records.first.class).to eql(Dnsruby::RR::IN::A)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "knows if a record exists" do
|
48
|
+
expect(subject.has_record?("A")).to eql(true)
|
49
|
+
expect(subject.has_record?("CNAME")).to eql(false)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "knows if a domain supports dnssec" do
|
53
|
+
expect(subject.dnssec?).to eql(false)
|
54
|
+
|
55
|
+
# via https://github.com/alexdalitz/dnsruby/blob/master/test/tc_dnskey.rb
|
56
|
+
input = "example.com. 86400 IN DNSKEY 256 3 5 ( AQPSKmynfzW4kyBv015MUG2DeIQ3" +
|
57
|
+
"Cbl+BBZH4b/0PY1kxkmvHjcZc8no" +
|
58
|
+
"kfzj31GajIQKY+5CptLr3buXA10h" +
|
59
|
+
"WqTkF7H6RfoRqXQeogmMHfpftf6z" +
|
60
|
+
"Mv1LyBUgia7za6ZEzOJBOztyvhjL" +
|
61
|
+
"742iU/TpPSEDhm2SNKLijfUppn1U" +
|
62
|
+
"aNvv4w== )"
|
63
|
+
|
64
|
+
record = Dnsruby::RR.create input
|
65
|
+
allow(subject).to receive(:records) { [record] }
|
66
|
+
|
67
|
+
expect(subject.dnssec?).to eql(true)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "knows if a domain supports ipv6" do
|
71
|
+
expect(subject.ipv6?).to eql(false)
|
72
|
+
|
73
|
+
input = {
|
74
|
+
:type => "AAAA",
|
75
|
+
:name => "test",
|
76
|
+
:address => '102:304:506:708:90a:b0c:d0e:ff10'
|
77
|
+
}
|
78
|
+
record = Dnsruby::RR.create input
|
79
|
+
allow(subject).to receive(:records) { [record] }
|
80
|
+
|
81
|
+
expect(subject.ipv6?).to eql(true)
|
82
|
+
end
|
83
|
+
|
84
|
+
context "hostname detection" do
|
85
|
+
it "lists cnames" do
|
86
|
+
records = []
|
87
|
+
|
88
|
+
records.push Dnsruby::RR.create({
|
89
|
+
:type => "CNAME",
|
90
|
+
:domainname => "example.com",
|
91
|
+
:name => "example"
|
92
|
+
})
|
93
|
+
|
94
|
+
records.push Dnsruby::RR.create({
|
95
|
+
:type => "CNAME",
|
96
|
+
:domainname => "github.com",
|
97
|
+
:name => "github"
|
98
|
+
})
|
99
|
+
|
100
|
+
allow(subject).to receive(:records) { records }
|
101
|
+
|
102
|
+
expect(subject.cnames.count).to eql(2)
|
103
|
+
expect(subject.cnames.first.sld).to eql("example")
|
104
|
+
end
|
105
|
+
|
106
|
+
it "knows when a domain doesn't have a cdn" do
|
107
|
+
expect(subject.cdn?).to eql(false)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "detects CDNs" do
|
111
|
+
records = [Dnsruby::RR.create({
|
112
|
+
:type => "CNAME",
|
113
|
+
:domainname => "foo.cloudfront.net",
|
114
|
+
:name => "example"
|
115
|
+
})]
|
116
|
+
allow(subject).to receive(:records) { records }
|
117
|
+
|
118
|
+
expect(subject.send(:detect_by_hostname, "cdn")).to eql(:cloudfront)
|
119
|
+
expect(subject.cdn).to eql(:cloudfront)
|
120
|
+
expect(subject.cdn?).to eql(true)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "builds that path to a data file" do
|
124
|
+
path = subject.send(:data_path, "foo")
|
125
|
+
expected = File.expand_path "../../lib/data/foo.yml", File.dirname(__FILE__)
|
126
|
+
expect(path).to eql(expected)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "loads data files" do
|
130
|
+
data = subject.send(:load_data, "cdn")
|
131
|
+
expect(data.keys).to include("cloudfront")
|
132
|
+
end
|
133
|
+
|
134
|
+
it "knows when a domain isn't cloud" do
|
135
|
+
expect(subject.cloud?).to eql(false)
|
136
|
+
end
|
137
|
+
|
138
|
+
it "detects cloud providers" do
|
139
|
+
records = [Dnsruby::RR.create({
|
140
|
+
:type => "CNAME",
|
141
|
+
:domainname => "foo.herokuapp.com",
|
142
|
+
:name => "example"
|
143
|
+
})]
|
144
|
+
allow(subject).to receive(:records) { records }
|
145
|
+
|
146
|
+
expect(subject.send(:detect_by_hostname, "cloud")).to eql(:heroku)
|
147
|
+
expect(subject.cloud_provider).to eql(:heroku)
|
148
|
+
expect(subject.cloud?).to eql(true)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "knows when a domain doesn't have google apps" do
|
152
|
+
expect(subject.google_apps?).to eql(false)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "knows when a domain is using google apps" do
|
156
|
+
records = [Dnsruby::RR.create({
|
157
|
+
:type => "MX",
|
158
|
+
:exchange => "mx1.google.com",
|
159
|
+
:name => "example",
|
160
|
+
:preference => 10
|
161
|
+
})]
|
162
|
+
allow(subject).to receive(:records) { records }
|
163
|
+
expect(subject.google_apps?).to eql(true)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SiteInspector::Endpoint::Headers do
|
4
|
+
|
5
|
+
subject do
|
6
|
+
stub_request(:get, "http://example.com/").
|
7
|
+
to_return(:status => 200, :headers => { :foo => "bar" } )
|
8
|
+
endpoint = SiteInspector::Endpoint.new("http://example.com")
|
9
|
+
SiteInspector::Endpoint::Headers.new(endpoint)
|
10
|
+
end
|
11
|
+
|
12
|
+
def stub_header(header, value)
|
13
|
+
allow(subject).to receive(:headers) { { header => value } }
|
14
|
+
end
|
15
|
+
|
16
|
+
it "parses the headers" do
|
17
|
+
expect(subject.headers.count).to eql(1)
|
18
|
+
expect(subject.headers.keys).to include("foo")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns a header" do
|
22
|
+
expect(subject["foo"]).to eql("bar")
|
23
|
+
expect(subject.headers["foo"]).to eql("bar")
|
24
|
+
end
|
25
|
+
|
26
|
+
it "knows the server" do
|
27
|
+
stub_header "server", "foo"
|
28
|
+
expect(subject.server).to eql("foo")
|
29
|
+
end
|
30
|
+
|
31
|
+
it "knows if a server has an xss protection header" do
|
32
|
+
stub_header "x-xss-protection", "foo"
|
33
|
+
expect(subject.xss_protection).to eql("foo")
|
34
|
+
end
|
35
|
+
|
36
|
+
it "validates xss-protection" do
|
37
|
+
stub_header "x-xss-protection", "foo"
|
38
|
+
expect(subject.xss_protection?).to eql(false)
|
39
|
+
|
40
|
+
stub_header "x-xss-protection", "1; mode=block"
|
41
|
+
expect(subject.xss_protection?).to eql(true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "checks for clickjack proetection" do
|
45
|
+
expect(subject.click_jacking_protection?).to eql(false)
|
46
|
+
stub_header "x-frame-options", "foo"
|
47
|
+
expect(subject.click_jacking_protection).to eql("foo")
|
48
|
+
expect(subject.click_jacking_protection?).to eql(true)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "checks for CSP" do
|
52
|
+
expect(subject.content_security_policy?).to eql(false)
|
53
|
+
stub_header "content-security-policy", "foo"
|
54
|
+
expect(subject.content_security_policy).to eql("foo")
|
55
|
+
expect(subject.content_security_policy?).to eql(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "checks for strict-transport-security" do
|
59
|
+
expect(subject.strict_transport_security?).to eql(false)
|
60
|
+
stub_header "strict-transport-security", "foo"
|
61
|
+
expect(subject.strict_transport_security).to eql("foo")
|
62
|
+
expect(subject.strict_transport_security?).to eql(true)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "knows if there are cookies" do
|
66
|
+
expect(subject.cookies?).to eql(false)
|
67
|
+
stub_header "set-cookie", "foo"
|
68
|
+
expect(subject.cookies?).to eql(true)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "knows if the cookies are secure" do
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|