site-inspector 1.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|