site-inspector 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ ]