site-inspector 3.1.1 → 3.2.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.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -1
  3. data/.rubocop.yml +18 -10
  4. data/.rubocop_todo.yml +139 -0
  5. data/.ruby-version +1 -1
  6. data/Gemfile +4 -0
  7. data/Guardfile +2 -0
  8. data/Rakefile +2 -0
  9. data/bin/site-inspector +7 -6
  10. data/lib/cliver/dependency_ext.rb +6 -3
  11. data/lib/site-inspector.rb +18 -11
  12. data/lib/site-inspector/cache.rb +2 -0
  13. data/lib/site-inspector/checks/accessibility.rb +30 -22
  14. data/lib/site-inspector/checks/check.rb +4 -2
  15. data/lib/site-inspector/checks/content.rb +15 -4
  16. data/lib/site-inspector/checks/cookies.rb +5 -3
  17. data/lib/site-inspector/checks/dns.rb +13 -11
  18. data/lib/site-inspector/checks/headers.rb +8 -6
  19. data/lib/site-inspector/checks/hsts.rb +16 -12
  20. data/lib/site-inspector/checks/https.rb +3 -1
  21. data/lib/site-inspector/checks/sniffer.rb +10 -7
  22. data/lib/site-inspector/checks/wappalyzer.rb +62 -0
  23. data/lib/site-inspector/checks/whois.rb +36 -0
  24. data/lib/site-inspector/disk_cache.rb +2 -0
  25. data/lib/site-inspector/domain.rb +36 -30
  26. data/lib/site-inspector/endpoint.rb +22 -23
  27. data/lib/site-inspector/rails_cache.rb +2 -0
  28. data/lib/site-inspector/version.rb +3 -1
  29. data/package-lock.json +505 -0
  30. data/package.json +1 -1
  31. data/script/pa11y-version +1 -0
  32. data/site-inspector.gemspec +24 -17
  33. data/spec/checks/site_inspector_endpoint_accessibility_spec.rb +15 -13
  34. data/spec/checks/site_inspector_endpoint_check_spec.rb +9 -7
  35. data/spec/checks/site_inspector_endpoint_content_spec.rb +30 -21
  36. data/spec/checks/site_inspector_endpoint_cookies_spec.rb +17 -15
  37. data/spec/checks/site_inspector_endpoint_dns_spec.rb +42 -40
  38. data/spec/checks/site_inspector_endpoint_headers_spec.rb +12 -10
  39. data/spec/checks/site_inspector_endpoint_hsts_spec.rb +27 -25
  40. data/spec/checks/site_inspector_endpoint_https_spec.rb +12 -10
  41. data/spec/checks/site_inspector_endpoint_sniffer_spec.rb +33 -31
  42. data/spec/checks/site_inspector_endpoint_wappalyzer_spec.rb +34 -0
  43. data/spec/checks/site_inspector_endpoint_whois_spec.rb +26 -0
  44. data/spec/fixtures/wappalyzer.json +125 -0
  45. data/spec/site_inspector_cache_spec.rb +2 -0
  46. data/spec/site_inspector_disk_cache_spec.rb +8 -6
  47. data/spec/site_inspector_domain_spec.rb +34 -34
  48. data/spec/site_inspector_endpoint_spec.rb +44 -43
  49. data/spec/site_inspector_spec.rb +15 -13
  50. data/spec/spec_helper.rb +2 -0
  51. metadata +125 -55
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe SiteInspector::Endpoint::Headers do
@@ -5,7 +7,7 @@ describe SiteInspector::Endpoint::Headers do
5
7
  stub_request(:head, 'http://example.com/')
6
8
  .to_return(status: 200, headers: { foo: 'bar' })
7
9
  endpoint = SiteInspector::Endpoint.new('http://example.com')
8
- SiteInspector::Endpoint::Headers.new(endpoint)
10
+ described_class.new(endpoint)
9
11
  end
10
12
 
11
13
  def stub_header(header, value)
@@ -13,7 +15,7 @@ describe SiteInspector::Endpoint::Headers do
13
15
  end
14
16
 
15
17
  it 'parses the headers' do
16
- expect(subject.headers.count).to eql(1)
18
+ expect(subject.headers.count).to be(1)
17
19
  expect(subject.headers.keys).to include('foo')
18
20
  end
19
21
 
@@ -34,30 +36,30 @@ describe SiteInspector::Endpoint::Headers do
34
36
 
35
37
  it 'validates xss-protection' do
36
38
  stub_header 'x-xss-protection', 'foo'
37
- expect(subject.xss_protection?).to eql(false)
39
+ expect(subject.xss_protection?).to be(false)
38
40
 
39
41
  stub_header 'x-xss-protection', '1; mode=block'
40
- expect(subject.xss_protection?).to eql(true)
42
+ expect(subject.xss_protection?).to be(true)
41
43
  end
42
44
 
43
45
  it 'checks for clickjack proetection' do
44
- expect(subject.click_jacking_protection?).to eql(false)
46
+ expect(subject.click_jacking_protection?).to be(false)
45
47
  stub_header 'x-frame-options', 'foo'
46
48
  expect(subject.click_jacking_protection).to eql('foo')
47
- expect(subject.click_jacking_protection?).to eql(true)
49
+ expect(subject.click_jacking_protection?).to be(true)
48
50
  end
49
51
 
50
52
  it 'checks for CSP' do
51
- expect(subject.content_security_policy?).to eql(false)
53
+ expect(subject.content_security_policy?).to be(false)
52
54
  stub_header 'content-security-policy', 'foo'
53
55
  expect(subject.content_security_policy).to eql('foo')
54
- expect(subject.content_security_policy?).to eql(true)
56
+ expect(subject.content_security_policy?).to be(true)
55
57
  end
56
58
 
57
59
  it 'checks for strict-transport-security' do
58
- expect(subject.strict_transport_security?).to eql(false)
60
+ expect(subject.strict_transport_security?).to be(false)
59
61
  stub_header 'strict-transport-security', 'foo'
60
62
  expect(subject.strict_transport_security).to eql('foo')
61
- expect(subject.strict_transport_security?).to eql(true)
63
+ expect(subject.strict_transport_security?).to be(true)
62
64
  end
63
65
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe SiteInspector::Endpoint::Hsts do
@@ -6,7 +8,7 @@ describe SiteInspector::Endpoint::Hsts do
6
8
  stub_request(:head, 'http://example.com/')
7
9
  .to_return(status: 200, headers: headers)
8
10
  endpoint = SiteInspector::Endpoint.new('http://example.com')
9
- SiteInspector::Endpoint::Hsts.new(endpoint)
11
+ described_class.new(endpoint)
10
12
  end
11
13
 
12
14
  def stub_header(value)
@@ -21,8 +23,8 @@ describe SiteInspector::Endpoint::Hsts do
21
23
  expect(subject.send(:header)).to eql('max-age=31536000; includeSubDomains;')
22
24
  end
23
25
 
24
- it 'it parses the directives' do
25
- expect(subject.send(:directives).count).to eql(2)
26
+ it 'parses the directives' do
27
+ expect(subject.send(:directives).count).to be(2)
26
28
  expect(subject.send(:directives).first).to eql('max-age=31536000')
27
29
  expect(subject.send(:directives).last).to eql('includeSubDomains')
28
30
  end
@@ -33,58 +35,58 @@ describe SiteInspector::Endpoint::Hsts do
33
35
  end
34
36
 
35
37
  it 'knows if the header is valid' do
36
- expect(subject.valid?).to eql(true)
38
+ expect(subject.valid?).to be(true)
37
39
 
38
- allow(subject).to receive(:pairs) { ['fo o' => 'bar'] }
39
- expect(subject.valid?).to eql(false)
40
+ allow(subject).to receive(:pairs).and_return(['fo o' => 'bar'])
41
+ expect(subject.valid?).to be(false)
40
42
 
41
- allow(subject).to receive(:pairs) { ["fo'o" => 'bar'] }
42
- expect(subject.valid?).to eql(false)
43
+ allow(subject).to receive(:pairs).and_return(["fo'o" => 'bar'])
44
+ expect(subject.valid?).to be(false)
43
45
  end
44
46
 
45
47
  it 'knows the max age' do
46
- expect(subject.max_age).to eql(31_536_000)
48
+ expect(subject.max_age).to be(31_536_000)
47
49
  end
48
50
 
49
51
  it 'knows if subdomains are included' do
50
- expect(subject.include_subdomains?).to eql(true)
51
- allow(subject).to receive(:pairs) { { foo: 'bar' } }
52
- expect(subject.include_subdomains?).to eql(false)
52
+ expect(subject.include_subdomains?).to be(true)
53
+ allow(subject).to receive(:pairs).and_return(foo: 'bar')
54
+ expect(subject.include_subdomains?).to be(false)
53
55
  end
54
56
 
55
57
  it "knows if it's preloaded" do
56
- expect(subject.preload?).to eql(false)
57
- allow(subject).to receive(:pairs) { { preload: nil } }
58
- expect(subject.preload?).to eql(true)
58
+ expect(subject.preload?).to be(false)
59
+ allow(subject).to receive(:pairs).and_return(preload: nil)
60
+ expect(subject.preload?).to be(true)
59
61
  end
60
62
 
61
63
  it "knows if it's enabled" do
62
- expect(subject.enabled?).to eql(true)
64
+ expect(subject.enabled?).to be(true)
63
65
 
64
- allow(subject).to receive(:pairs) { { "max-age": 0 } }
65
- expect(subject.preload?).to eql(false)
66
+ allow(subject).to receive(:pairs).and_return("max-age": 0)
67
+ expect(subject.preload?).to be(false)
66
68
 
67
- allow(subject).to receive(:pairs) { { foo: 'bar' } }
68
- expect(subject.preload?).to eql(false)
69
+ allow(subject).to receive(:pairs).and_return(foo: 'bar')
70
+ expect(subject.preload?).to be(false)
69
71
  end
70
72
 
71
73
  it "knows if it's preload ready" do
72
- expect(subject.preload_ready?).to eql(false)
74
+ expect(subject.preload_ready?).to be(false)
73
75
 
74
76
  pairs = { "max-age": 10_886_401, preload: nil, includesubdomains: nil }
75
77
  allow(subject).to receive(:pairs) { pairs }
76
- expect(subject.preload_ready?).to eql(true)
78
+ expect(subject.preload_ready?).to be(true)
77
79
 
78
80
  pairs = { "max-age": 10_886_401, includesubdomains: nil }
79
81
  allow(subject).to receive(:pairs) { pairs }
80
- expect(subject.preload_ready?).to eql(false)
82
+ expect(subject.preload_ready?).to be(false)
81
83
 
82
84
  pairs = { "max-age": 10_886_401, preload: nil, includesubdomains: nil }
83
85
  allow(subject).to receive(:pairs) { pairs }
84
- expect(subject.preload_ready?).to eql(true)
86
+ expect(subject.preload_ready?).to be(true)
85
87
 
86
88
  pairs = { "max-age": 5, preload: nil, includesubdomains: nil }
87
89
  allow(subject).to receive(:pairs) { pairs }
88
- expect(subject.preload_ready?).to eql(false)
90
+ expect(subject.preload_ready?).to be(false)
89
91
  end
90
92
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe SiteInspector::Endpoint::Https do
@@ -5,8 +7,8 @@ describe SiteInspector::Endpoint::Https do
5
7
  stub_request(:head, 'https://example.com/')
6
8
  .to_return(status: 200)
7
9
  endpoint = SiteInspector::Endpoint.new('https://example.com')
8
- allow(endpoint.response).to receive(:return_code) { :ok }
9
- SiteInspector::Endpoint::Https.new(endpoint)
10
+ allow(endpoint.response).to receive(:return_code).and_return(:ok)
11
+ described_class.new(endpoint)
10
12
  end
11
13
 
12
14
  it 'knows the scheme' do
@@ -14,34 +16,34 @@ describe SiteInspector::Endpoint::Https do
14
16
  end
15
17
 
16
18
  it 'knows if the scheme is https' do
17
- expect(subject.scheme?).to eql(true)
18
- allow(subject).to receive(:scheme) { 'http' }
19
- expect(subject.scheme?).to eql(false)
19
+ expect(subject.scheme?).to be(true)
20
+ allow(subject).to receive(:scheme).and_return('http')
21
+ expect(subject.scheme?).to be(false)
20
22
  end
21
23
 
22
24
  it "knows if it's valid" do
23
- expect(subject.valid?).to eql(true)
25
+ expect(subject.valid?).to be(true)
24
26
  end
25
27
 
26
28
  it "knows when there's a bad chain" do
27
- expect(subject.bad_chain?).to eql(false)
29
+ expect(subject.bad_chain?).to be(false)
28
30
 
29
31
  url = Addressable::URI.parse('https://example.com')
30
32
  response = Typhoeus::Response.new(return_code: :ssl_cacert)
31
33
  response.request = Typhoeus::Request.new(url)
32
34
 
33
35
  allow(subject).to receive(:response) { response }
34
- expect(subject.bad_chain?).to eql(true)
36
+ expect(subject.bad_chain?).to be(true)
35
37
  end
36
38
 
37
39
  it "knows when there's a bad name" do
38
- expect(subject.bad_name?).to eql(false)
40
+ expect(subject.bad_name?).to be(false)
39
41
 
40
42
  url = Addressable::URI.parse('https://example.com')
41
43
  response = Typhoeus::Response.new(return_code: :peer_failed_verification)
42
44
  response.request = Typhoeus::Request.new(url)
43
45
 
44
46
  allow(subject).to receive(:response) { response }
45
- expect(subject.bad_name?).to eql(true)
47
+ expect(subject.bad_name?).to be(true)
46
48
  end
47
49
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe SiteInspector::Endpoint::Sniffer do
@@ -8,16 +10,16 @@ describe SiteInspector::Endpoint::Sniffer do
8
10
  def set_cookie(key, value)
9
11
  cookies = [
10
12
  CGI::Cookie.new(
11
- 'name' => 'foo',
12
- 'value' => 'bar',
13
+ 'name' => 'foo',
14
+ 'value' => 'bar',
13
15
  'domain' => 'example.com',
14
- 'path' => '/'
16
+ 'path' => '/'
15
17
  ),
16
18
  CGI::Cookie.new(
17
- 'name' => key,
18
- 'value' => value,
19
+ 'name' => key,
20
+ 'value' => value,
19
21
  'domain' => 'example.com',
20
- 'path' => '/'
22
+ 'path' => '/'
21
23
  )
22
24
  ].map(&:to_s)
23
25
 
@@ -30,7 +32,7 @@ describe SiteInspector::Endpoint::Sniffer do
30
32
 
31
33
  context 'stubbed body' do
32
34
  subject do
33
- body = <<-eos
35
+ body = <<-BODY
34
36
  <html>
35
37
  <head>
36
38
  <link rel='stylesheet' href='/wp-content/themes/foo/style.css type='text/css' media='all' />
@@ -48,7 +50,7 @@ describe SiteInspector::Endpoint::Sniffer do
48
50
  </script>
49
51
  </body>
50
52
  </html>
51
- eos
53
+ BODY
52
54
 
53
55
  stub_request(:get, 'http://example.com/')
54
56
  .to_return(status: 200, body: body)
@@ -56,56 +58,56 @@ describe SiteInspector::Endpoint::Sniffer do
56
58
  stub_request(:head, 'http://example.com/')
57
59
  .to_return(status: 200)
58
60
  endpoint = SiteInspector::Endpoint.new('http://example.com')
59
- SiteInspector::Endpoint::Sniffer.new(endpoint)
61
+ described_class.new(endpoint)
60
62
  end
61
63
 
62
64
  it 'sniffs' do
63
65
  sniff = subject.send(:sniff, :cms)
64
- expect(sniff).to eql(:wordpress)
66
+ expect(sniff).to be(:wordpress)
65
67
  end
66
68
 
67
69
  it 'detects the CMS' do
68
- expect(subject.framework).to eql(:wordpress)
70
+ expect(subject.framework).to be(:wordpress)
69
71
  end
70
72
 
71
73
  it 'detects the analytics' do
72
- expect(subject.analytics).to eql(:google_analytics)
74
+ expect(subject.analytics).to be(:google_analytics)
73
75
  end
74
76
 
75
77
  it 'detects javascript' do
76
- expect(subject.javascript).to eql(:jquery)
78
+ expect(subject.javascript).to be(:jquery)
77
79
  end
78
80
 
79
81
  it 'detects advertising' do
80
- expect(subject.advertising).to eql(:adsense)
82
+ expect(subject.advertising).to be(:adsense)
81
83
  end
82
84
 
83
85
  it 'knows wordpress is open source' do
84
- expect(subject.open_source?).to eql(true)
86
+ expect(subject.open_source?).to be(true)
85
87
  end
86
88
  end
87
89
 
88
90
  context 'no body' do
89
91
  subject do
90
92
  endpoint = SiteInspector::Endpoint.new('http://example.com')
91
- SiteInspector::Endpoint::Sniffer.new(endpoint)
93
+ described_class.new(endpoint)
92
94
  end
93
95
 
94
96
  it "knows when something isn't open source" do
95
97
  set_cookie('foo', 'bar')
96
- expect(subject.open_source?).to eql(false)
98
+ expect(subject.open_source?).to be(false)
97
99
  end
98
100
 
99
101
  it 'detects PHP' do
100
102
  set_cookie('PHPSESSID', '1234')
101
- expect(subject.framework).to eql(:php)
102
- expect(subject.open_source?).to eql(true)
103
+ expect(subject.framework).to be(:php)
104
+ expect(subject.open_source?).to be(true)
103
105
  end
104
106
 
105
107
  it 'detects Expression Engine' do
106
108
  set_cookie('exp_csrf_token', '1234')
107
- expect(subject.framework).to eql(:expression_engine)
108
- expect(subject.open_source?).to eql(true)
109
+ expect(subject.framework).to be(:expression_engine)
110
+ expect(subject.open_source?).to be(true)
109
111
  end
110
112
 
111
113
  it 'detects cowboy' do
@@ -115,23 +117,23 @@ describe SiteInspector::Endpoint::Sniffer do
115
117
  stub_request(:head, 'http://example.com/')
116
118
  .to_return(status: 200, headers: { 'server' => 'Cowboy' })
117
119
 
118
- expect(subject.framework).to eql(:cowboy)
119
- expect(subject.open_source?).to eql(true)
120
+ expect(subject.framework).to be(:cowboy)
121
+ expect(subject.open_source?).to be(true)
120
122
  end
121
123
 
122
124
  it 'detects ColdFusion' do
123
125
  cookies = [
124
126
  CGI::Cookie.new(
125
- 'name' => 'CFID',
126
- 'value' => '1234',
127
+ 'name' => 'CFID',
128
+ 'value' => '1234',
127
129
  'domain' => 'example.com',
128
- 'path' => '/'
130
+ 'path' => '/'
129
131
  ),
130
132
  CGI::Cookie.new(
131
- 'name' => 'CFTOKEN',
132
- 'value' => '5678',
133
+ 'name' => 'CFTOKEN',
134
+ 'value' => '5678',
133
135
  'domain' => 'example.com',
134
- 'path' => '/'
136
+ 'path' => '/'
135
137
  )
136
138
  ].map(&:to_s)
137
139
 
@@ -141,8 +143,8 @@ describe SiteInspector::Endpoint::Sniffer do
141
143
  stub_request(:head, 'http://example.com/')
142
144
  .to_return(status: 200, headers: { 'set-cookie' => cookies })
143
145
 
144
- expect(subject.framework).to eql(:coldfusion)
145
- expect(subject.open_source?).to eql(false)
146
+ expect(subject.framework).to be(:coldfusion)
147
+ expect(subject.open_source?).to be(false)
146
148
  end
147
149
  end
148
150
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SiteInspector::Endpoint::Wappalyzer do
6
+ subject { described_class.new(endpoint) }
7
+
8
+ let(:domain) { 'http://ben.balter.com.com' }
9
+ let(:endpoint) { SiteInspector::Endpoint.new(domain) }
10
+ let(:url) { "https://api.wappalyzer.com/lookup/v2/?urls=#{domain}/" }
11
+
12
+ before do
13
+ path = File.expand_path '../fixtures/wappalyzer.json', __dir__
14
+ body = File.read path
15
+ stub_request(:get, url).to_return(status: 200, body: body)
16
+ end
17
+
18
+ it 'returns the API response' do
19
+ expected = {
20
+ 'Analytics' => ['Google Analytics'],
21
+ 'CDN' => %w[Cloudflare Fastly],
22
+ 'Caching' => ['Varnish'],
23
+ 'Other' => %w[Disqus Jekyll],
24
+ 'PaaS' => ['GitHub Pages'],
25
+ 'Web frameworks' => ['Ruby on Rails']
26
+ }
27
+ expect(subject.to_h).to eql(expected)
28
+ end
29
+
30
+ it 'fails gracefully' do
31
+ stub_request(:get, url).to_return(status: 400, body: '')
32
+ expect(subject.to_h).to eql({})
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SiteInspector::Endpoint::Whois do
6
+ subject do
7
+ stub_request(:head, site).to_return(status: 200)
8
+ endpoint = SiteInspector::Endpoint.new(site)
9
+ described_class.new(endpoint)
10
+ end
11
+
12
+ let(:site) { 'https://example.com' }
13
+
14
+ it 'returns the whois for the IP' do
15
+ expect(subject.ip).to match(/Derrick Sawyer/)
16
+ end
17
+
18
+ it 'returns the whois for the domain' do
19
+ expect(subject.domain).to match(/Domain Name: EXAMPLE\.COM/)
20
+ end
21
+
22
+ it 'returns the hash' do
23
+ expect(subject.to_h[:domain].keys.first).to eql('Domain Name')
24
+ expect(subject.to_h[:domain].values.first).to eql('EXAMPLE.COM')
25
+ end
26
+ end
@@ -0,0 +1,125 @@
1
+ [
2
+ {
3
+ "url":"https://ben.balter.com",
4
+ "technologies":[
5
+ {
6
+ "slug":"cloudflare",
7
+ "name":"Cloudflare",
8
+ "versions":[
9
+
10
+ ],
11
+ "trafficRank":11,
12
+ "categories":[
13
+ {
14
+ "id":31,
15
+ "slug":"cdn",
16
+ "name":"CDN"
17
+ }
18
+ ]
19
+ },
20
+ {
21
+ "slug":"varnish",
22
+ "name":"Varnish",
23
+ "versions":[
24
+
25
+ ],
26
+ "trafficRank":11,
27
+ "categories":[
28
+ {
29
+ "id":23,
30
+ "slug":"caching",
31
+ "name":"Caching"
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ "slug":"disqus",
37
+ "name":"Disqus",
38
+ "versions":[
39
+
40
+ ],
41
+ "trafficRank":11,
42
+ "categories":[
43
+
44
+ ]
45
+ },
46
+ {
47
+ "slug":"google-analytics",
48
+ "name":"Google Analytics",
49
+ "versions":[
50
+
51
+ ],
52
+ "trafficRank":11,
53
+ "categories":[
54
+ {
55
+ "id":10,
56
+ "slug":"analytics",
57
+ "name":"Analytics"
58
+ },
59
+ {
60
+ "id":61,
61
+ "slug":"saas",
62
+ "name":"SaaS"
63
+ }
64
+ ]
65
+ },
66
+ {
67
+ "slug":"jekyll",
68
+ "name":"Jekyll",
69
+ "versions":[
70
+ "v3.9.0"
71
+ ],
72
+ "trafficRank":11,
73
+ "categories":[
74
+
75
+ ]
76
+ },
77
+ {
78
+ "slug":"ruby-on-rails",
79
+ "name":"Ruby on Rails",
80
+ "versions":[
81
+
82
+ ],
83
+ "trafficRank":11,
84
+ "categories":[
85
+ {
86
+ "id":18,
87
+ "slug":"web-frameworks",
88
+ "name":"Web frameworks"
89
+ }
90
+ ]
91
+ },
92
+ {
93
+ "slug":"fastly",
94
+ "name":"Fastly",
95
+ "versions":[
96
+
97
+ ],
98
+ "trafficRank":11,
99
+ "categories":[
100
+ {
101
+ "id":31,
102
+ "slug":"cdn",
103
+ "name":"CDN"
104
+ }
105
+ ]
106
+ },
107
+ {
108
+ "slug":"github-pages",
109
+ "name":"GitHub Pages",
110
+ "versions":[
111
+
112
+ ],
113
+ "trafficRank":11,
114
+ "categories":[
115
+ {
116
+ "id":62,
117
+ "slug":"paas",
118
+ "name":"PaaS"
119
+ }
120
+ ]
121
+ }
122
+ ],
123
+ "crawl":true
124
+ }
125
+ ]