shodanz 1.0.6 → 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,9 +1,16 @@
1
- require 'unirest'
2
- require 'oj'
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'async'
5
+ require 'async/http/internet'
3
6
  require 'shodanz/version'
7
+ require 'shodanz/errors'
4
8
  require 'shodanz/api'
5
9
  require 'shodanz/client'
6
10
 
11
+ # disable async logs by default
12
+ Async.logger.level = 4
13
+
7
14
  # Shodanz is a modern Ruby gem for Shodan, the world's
8
15
  # first search engine for Internet-connected devices.
9
16
  module Shodanz
@@ -1,3 +1,7 @@
1
+ require_relative 'utils.rb'
2
+
3
+ # frozen_string_literal: true
4
+
1
5
  module Shodanz
2
6
  module API
3
7
  # The Exploits API provides access to several exploit
@@ -9,16 +13,21 @@ module Shodanz
9
13
  #
10
14
  # @author Kent 'picat' Gruber
11
15
  class Exploits
16
+ include Shodanz::API::Utils
17
+
12
18
  # @return [String]
13
19
  attr_accessor :key
14
20
 
15
21
  # The path to the REST API endpoint.
16
- URL = 'https://exploits.shodan.io/api/'.freeze
22
+ URL = 'https://exploits.shodan.io/'
17
23
 
18
24
  # @param key [String] SHODAN API key, defaulted to
19
25
  # the *SHODAN_API_KEY* enviroment variable.
20
26
  def initialize(key: ENV['SHODAN_API_KEY'])
21
- self.key = key
27
+ @url = URL
28
+ @client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse(@url))
29
+ self.key = key
30
+
22
31
  warn 'No key has been found or provided!' unless key?
23
32
  end
24
33
 
@@ -26,6 +35,7 @@ module Shodanz
26
35
  # @return [String]
27
36
  def key?
28
37
  return true if @key
38
+
29
39
  false
30
40
  end
31
41
 
@@ -35,53 +45,24 @@ module Shodanz
35
45
  # api.search("SQL", port: 443)
36
46
  # api.search(port: 22)
37
47
  # api.search(type: "dos")
38
- def search(query = '', page: 1, **params)
48
+ def search(query = '', facets: {}, page: 1, **params)
39
49
  params[:query] = query
40
- params = turn_into_query(params)
41
- facets = turn_into_facets(facets)
50
+ params = turn_into_query(**params)
51
+ facets = turn_into_facets(**facets)
42
52
  params[:page] = page
43
- get('search', params.merge(facets))
53
+ get('api/search', **params.merge(**facets))
44
54
  end
45
55
 
46
56
  # This method behaves identical to the "/search" method with
47
57
  # the difference that it doesn't return any results.
48
58
  # == Example
49
59
  # api.count(type: "dos")
50
- def count(query = '', page: 1, **params)
60
+ def count(query = '', facets: {}, page: 1, **params)
51
61
  params[:query] = query
52
- params = turn_into_query(params)
53
- facets = turn_into_facets(params)
62
+ params = turn_into_query(**params)
63
+ facets = turn_into_facets(**facets)
54
64
  params[:page] = page
55
- get('count', params.merge(facets))
56
- end
57
-
58
- # Perform a direct GET HTTP request to the REST API.
59
- def get(path, **params)
60
- resp = Unirest.get "#{URL}#{path}?key=#{@key}", parameters: params
61
- if resp.code != 200 && resp.body.key?('error')
62
- raise resp.body['error']
63
- end
64
- resp.body
65
- end
66
-
67
- private
68
-
69
- def turn_into_query(params)
70
- filters = params.reject { |key, _| key == :query }
71
- filters.each do |key, value|
72
- params[:query] << " #{key}:#{value}"
73
- end
74
- params.select { |key, _| key == :query }
75
- end
76
-
77
- def turn_into_facets(facets)
78
- filters = facets.reject { |key, _| key == :facets }
79
- facets[:facets] = []
80
- filters.each do |key, value|
81
- facets[:facets] << "#{key}:#{value}"
82
- end
83
- facets[:facets] = facets[:facets].join(',')
84
- facets.select { |key, _| key == :facets }
65
+ get('api/count', **params.merge(**facets))
85
66
  end
86
67
  end
87
68
  end
@@ -1,27 +1,38 @@
1
- module Shodanz
1
+ require_relative 'utils.rb'
2
+
3
+ # frozen_string_literal: true
2
4
 
5
+ module Shodanz
3
6
  module API
4
- # The REST API provides methods to search Shodan, look up
5
- # hosts, get summary information on queries and a variety
7
+ # The REST API provides methods to search Shodan, look up
8
+ # hosts, get summary information on queries and a variety
6
9
  # of other utilities. This requires you to have an API key
7
10
  # which you can get from Shodan.
8
11
  #
9
12
  # @author Kent 'picat' Gruber
10
13
  class REST
14
+ include Shodanz::API::Utils
15
+
16
+ # @return [String]
11
17
  attr_accessor :key
12
18
 
13
19
  # The path to the REST API endpoint.
14
- URL = "https://api.shodan.io/"
20
+ URL = 'https://api.shodan.io/'
15
21
 
16
22
  # @param key [String] SHODAN API key, defaulted to the *SHODAN_API_KEY* enviroment variable.
17
23
  def initialize(key: ENV['SHODAN_API_KEY'])
18
- self.key = key
19
- warn "No key has been found or provided!" unless self.key?
24
+ @url = URL
25
+ @client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse(@url))
26
+ self.key = key
27
+
28
+ warn 'No key has been found or provided!' unless key?
20
29
  end
21
30
 
22
31
  # Check if there's an API key.
23
32
  def key?
24
- return true if @key; false
33
+ return true if @key
34
+
35
+ false
25
36
  end
26
37
 
27
38
  # Returns all services that have been found on the given host IP.
@@ -31,113 +42,112 @@ module Shodanz
31
42
  # == Examples
32
43
  # # Typical usage.
33
44
  # rest_api.host("8.8.8.8")
34
- #
45
+ #
35
46
  # # All historical banners should be returned.
36
- # rest_api.host("8.8.8.8", history: true)
47
+ # rest_api.host("8.8.8.8", history: true)
37
48
  #
38
49
  # # Only return the list of ports and the general host information, no banners.
39
- # rest_api.host("8.8.8.8", minify: true)
50
+ # rest_api.host("8.8.8.8", minify: true)
40
51
  def host(ip, **params)
41
- get("shodan/host/#{ip}", params)
52
+ get("shodan/host/#{ip}", **params)
42
53
  end
43
54
 
44
- # This method behaves identical to "/shodan/host/search" with the only
45
- # difference that this method does not return any host results, it only
46
- # returns the total number of results that matched the query and any
47
- # facet information that was requested. As a result this method does
55
+ # This method behaves identical to "/shodan/host/search" with the only
56
+ # difference that this method does not return any host results, it only
57
+ # returns the total number of results that matched the query and any
58
+ # facet information that was requested. As a result this method does
48
59
  # not consume query credits.
49
60
  # == Examples
50
61
  # rest_api.host_count("apache")
51
62
  # rest_api.host_count("apache", country: "US")
52
63
  # rest_api.host_count("apache", country: "US", state: "MI")
53
64
  # rest_api.host_count("apache", country: "US", state: "MI", city: "Detroit")
54
- def host_count(query = "", facets: {}, **params)
65
+ def host_count(query = '', facets: {}, **params)
55
66
  params[:query] = query
56
- params = turn_into_query(params)
57
- facets = turn_into_facets(facets)
58
- get("shodan/host/count", params.merge(facets))
67
+ params = turn_into_query(**params)
68
+ facets = turn_into_facets(**facets)
69
+ get('shodan/host/count', **params.merge(**facets))
59
70
  end
60
71
 
61
- # Search Shodan using the same query syntax as the website and use facets
72
+ # Search Shodan using the same query syntax as the website and use facets
62
73
  # to get summary information for different properties.
63
74
  # == Example
64
75
  # rest_api.host_search("apache", country: "US", facets: { city: "Detroit" }, page: 1, minify: false)
65
- def host_search(query = "", facets: {}, page: 1, minify: true, **params)
76
+ def host_search(query = '', facets: {}, page: 1, minify: true, **params)
66
77
  params[:query] = query
67
- params = turn_into_query(params)
68
- facets = turn_into_facets(facets)
78
+ params = turn_into_query(**params)
79
+ facets = turn_into_facets(**facets)
69
80
  params[:page] = page
70
81
  params[:minify] = minify
71
- get("shodan/host/search", params.merge(facets))
82
+ get('shodan/host/search', **params.merge(**facets))
72
83
  end
73
84
 
74
- # This method lets you determine which filters are being used by
85
+ # This method lets you determine which filters are being used by
75
86
  # the query string and what parameters were provided to the filters.
76
- def host_search_tokens(query = "", **params)
87
+ def host_search_tokens(query = '', **params)
77
88
  params[:query] = query
78
- params = turn_into_query(params)
79
- get("shodan/host/search/tokens", params)
89
+ params = turn_into_query(**params)
90
+ get('shodan/host/search/tokens', **params)
80
91
  end
81
92
 
82
93
  # This method returns a list of port numbers that the crawlers are looking for.
83
94
  def ports
84
- get("shodan/ports")
95
+ get('shodan/ports')
85
96
  end
86
97
 
87
98
  # List all protocols that can be used when performing on-demand Internet scans via Shodan.
88
- def protocols
89
- get("shodan/protocols")
99
+ def protocols
100
+ get('shodan/protocols')
90
101
  end
91
102
 
92
103
  # Use this method to request Shodan to crawl a network.
93
104
  #
94
- # This method uses API scan credits: 1 IP consumes 1 scan credit. You
95
- # must have a paid API plan (either one-time payment or subscription)
105
+ # This method uses API scan credits: 1 IP consumes 1 scan credit. You
106
+ # must have a paid API plan (either one-time payment or subscription)
96
107
  # in order to use this method.
97
108
  #
98
109
  # IP, IPs or netblocks (in CIDR notation) that should get crawled.
99
110
  def scan(*ips)
100
- raise "Not enough scan credits!" unless self.info["scan_credits"] >= 1
101
- post("shodan/scan", ips: ips.join(","))
111
+ post('shodan/scan', ips: ips.join(','))
102
112
  end
103
113
 
104
114
  # Use this method to request Shodan to crawl the Internet for a specific port.
105
115
  #
106
- # This method is restricted to security researchers and companies with
107
- # a Shodan Data license. To apply for access to this method as a researcher,
108
- # please email jmath@shodan.io with information about your project.
116
+ # This method is restricted to security researchers and companies with
117
+ # a Shodan Data license. To apply for access to this method as a researcher,
118
+ # please email jmath@shodan.io with information about your project.
109
119
  # Access is restricted to prevent abuse.
110
120
  #
111
121
  # == Example
112
122
  # rest_api.crawl_for(port: 80, protocol: "http")
113
123
  def crawl_for(**params)
114
- params[:query] = ""
115
- params = turn_into_query(params)
116
- post('shodan/scan/internet', params)
124
+ params[:query] = ''
125
+ params = turn_into_query(**params)
126
+ post('shodan/scan/internet', **params)
117
127
  end
118
128
 
119
- # Check the progress of a previously submitted scan request.
129
+ # Check the progress of a previously submitted scan request.
120
130
  def scan_status(id)
121
131
  get("shodan/scan/#{id}")
122
132
  end
123
133
 
124
134
  # Use this method to obtain a list of search queries that users have saved in Shodan.
125
135
  def community_queries(**params)
126
- get('shodan/query', params)
136
+ get('shodan/query', **params)
127
137
  end
128
138
 
129
139
  # Use this method to search the directory of search queries that users have saved in Shodan.
130
140
  def search_for_community_query(query, **params)
131
141
  params[:query] = query
132
- params = turn_into_query(params)
133
- get('shodan/query/search', params)
142
+ params = turn_into_query(**params)
143
+ get('shodan/query/search', **params)
134
144
  end
135
145
 
136
146
  # Use this method to obtain a list of popular tags for the saved search queries in Shodan.
137
147
  def popular_query_tags(size = 10)
138
148
  params = {}
139
149
  params[:size] = size
140
- get('shodan/query/tags', params)
150
+ get('shodan/query/tags', **params)
141
151
  end
142
152
 
143
153
  # Returns information about the Shodan account linked to this API key.
@@ -147,16 +157,16 @@ module Shodanz
147
157
 
148
158
  # Look up the IP address for the provided list of hostnames.
149
159
  def resolve(*hostnames)
150
- get('dns/resolve', hostnames: hostnames.join(","))
160
+ get('dns/resolve', hostnames: hostnames.join(','))
151
161
  end
152
162
 
153
- # Look up the hostnames that have been defined for the
163
+ # Look up the hostnames that have been defined for the
154
164
  # given list of IP addresses.
155
165
  def reverse_lookup(*ips)
156
- get('dns/reverse', ips: ips.join(","))
166
+ get('dns/reverse', ips: ips.join(','))
157
167
  end
158
168
 
159
- # Shows the HTTP headers that your client sends when
169
+ # Shows the HTTP headers that your client sends when
160
170
  # connecting to a webserver.
161
171
  def http_headers
162
172
  get('tools/httpheaders')
@@ -167,7 +177,7 @@ module Shodanz
167
177
  get('tools/myip')
168
178
  end
169
179
 
170
- # Calculates a honeypot probability score ranging
180
+ # Calculates a honeypot probability score ranging
171
181
  # from 0 (not a honeypot) to 1.0 (is a honeypot).
172
182
  def honeypot_score(ip)
173
183
  get("labs/honeyscore/#{ip}")
@@ -177,46 +187,6 @@ module Shodanz
177
187
  def info
178
188
  get('api-info')
179
189
  end
180
-
181
- # Perform a direct GET HTTP request to the REST API.
182
- def get(path, **params)
183
- resp = Unirest.get "#{URL}#{path}?key=#{@key}", parameters: params
184
- if resp.code != 200
185
- raise resp.body['error'] if resp.body.key?('error')
186
- raise resp
187
- end
188
- resp.body
189
- end
190
-
191
- # Perform a direct POST HTTP request to the REST API.
192
- def post(path, **params)
193
- resp = Unirest.post "#{URL}#{path}?key=#{@key}", parameters: params
194
- if resp.code != 200
195
- raise resp.body['error'] if resp.body.key?('error')
196
- raise resp
197
- end
198
- resp.body
199
- end
200
-
201
- private
202
-
203
- def turn_into_query(params)
204
- filters = params.reject { |key, _| key == :query }
205
- filters.each do |key, value|
206
- params[:query] << " #{key}:#{value}"
207
- end
208
- params.select { |key, _| key == :query }
209
- end
210
-
211
- def turn_into_facets(facets)
212
- filters = facets.reject { |key, _| key == :facets }
213
- facets[:facets] = []
214
- filters.each do |key, value|
215
- facets[:facets] << "#{key}:#{value}"
216
- end
217
- facets[:facets] = facets[:facets].join(',')
218
- facets.select { |key, _| key == :facets }
219
- end
220
190
  end
221
191
  end
222
192
  end
@@ -1,38 +1,48 @@
1
- module Shodanz
1
+ require_relative 'utils.rb'
2
+
3
+ # frozen_string_literal: true
2
4
 
5
+ module Shodanz
3
6
  module API
4
- # The REST API provides methods to search Shodan, look up
5
- # hosts, get summary information on queries and a variety
7
+ # The REST API provides methods to search Shodan, look up
8
+ # hosts, get summary information on queries and a variety
6
9
  # of other utilities. This requires you to have an API key
7
10
  # which you can get from Shodan.
8
11
  #
9
- # Note: Only 1-5% of the data is currently provided to
10
- # subscription-based API plans. If your company is interested
11
- # in large-scale, real-time access to all of the Shodan data
12
+ # Note: Only 1-5% of the data is currently provided to
13
+ # subscription-based API plans. If your company is interested
14
+ # in large-scale, real-time access to all of the Shodan data
12
15
  # please contact us for pricing information (sales@shodan.io).
13
- #
16
+ #
14
17
  # @author Kent 'picat' Gruber
15
18
  class Streaming
19
+ include Shodanz::API::Utils
20
+
21
+ # @return [String]
16
22
  attr_accessor :key
17
23
 
18
- # The Streaming API is an HTTP-based service that returns
24
+ # The Streaming API is an HTTP-based service that returns
19
25
  # a real-time stream of data collected by Shodan.
20
- URL = "https://stream.shodan.io/"
26
+ URL = 'https://stream.shodan.io/'
21
27
 
22
28
  # @param key [String] SHODAN API key, defaulted to the *SHODAN_API_KEY* enviroment variable.
23
29
  def initialize(key: ENV['SHODAN_API_KEY'])
24
- self.key = key
25
- warn "No key has been found or provided!" unless self.key?
30
+ @url = URL
31
+ @client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse(@url))
32
+ self.key = key
33
+
34
+ warn 'No key has been found or provided!' unless key?
26
35
  end
27
36
 
28
37
  # Check if there's an API key.
29
38
  def key?
30
39
  return true if @key; false
40
+
31
41
  end
32
42
 
33
- # This stream provides ALL of the data that Shodan collects.
34
- # Use this stream if you need access to everything and/ or want to
35
- # store your own Shodan database locally. If you only care about specific
43
+ # This stream provides ALL of the data that Shodan collects.
44
+ # Use this stream if you need access to everything and/ or want to
45
+ # store your own Shodan database locally. If you only care about specific
36
46
  # ports, please use the Ports stream.
37
47
  #
38
48
  # Sometimes data may be piped down stream that is weird to parse. You can choose
@@ -44,28 +54,28 @@ module Shodanz
44
54
  # puts data
45
55
  # end
46
56
  def banners(**params)
47
- slurp_stream("shodan/banners", params) do |data|
57
+ slurp_stream('shodan/banners', **params) do |data|
48
58
  yield data
49
59
  end
50
60
  end
51
61
 
52
- # This stream provides a filtered, bandwidth-saving view of the Banners
62
+ # This stream provides a filtered, bandwidth-saving view of the Banners
53
63
  # stream in case you are only interested in devices located in certain ASNs.
54
64
  # == Example
55
- # api.banners_within_asns(3303, 32475) do |data|
65
+ # api.banners_within_asns(3303, 32475) do |data|
56
66
  # # do something with the banner hash
57
67
  # puts data
58
68
  # end
59
69
  def banners_within_asns(*asns, **params)
60
- slurp_stream("shodan/asn/#{asns.join(",")}", params) do |data|
70
+ slurp_stream("shodan/asn/#{asns.join(',')}", **params) do |data|
61
71
  yield data
62
72
  end
63
73
  end
64
74
 
65
- # This stream provides a filtered, bandwidth-saving view of the Banners
75
+ # This stream provides a filtered, bandwidth-saving view of the Banners
66
76
  # stream in case you are only interested in devices located in a certain ASN.
67
77
  # == Example
68
- # api.banners_within_asn(3303) do |data|
78
+ # api.banners_within_asn(3303) do |data|
69
79
  # # do something with the banner hash
70
80
  # puts data
71
81
  # end
@@ -75,38 +85,38 @@ module Shodanz
75
85
  end
76
86
  end
77
87
 
78
- # Only returns banner data for the list of specified ports. This
79
- # stream provides a filtered, bandwidth-saving view of the Banners
88
+ # Only returns banner data for the list of specified ports. This
89
+ # stream provides a filtered, bandwidth-saving view of the Banners
80
90
  # stream in case you are only interested in a specific list of ports.
81
91
  # == Example
82
- # api.banners_within_countries("US","DE","JP") do |data|
92
+ # api.banners_within_countries("US","DE","JP") do |data|
83
93
  # # do something with the banner hash
84
94
  # puts data
85
95
  # end
86
96
  def banners_within_countries(*params)
87
- slurp_stream("shodan/countries/#{params.join(",")}") do |data|
97
+ slurp_stream("shodan/countries/#{params.join(',')}") do |data|
88
98
  yield data
89
99
  end
90
100
  end
91
101
 
92
- # Only returns banner data for the list of specified ports. This
93
- # stream provides a filtered, bandwidth-saving view of the
94
- # Banners stream in case you are only interested in a
102
+ # Only returns banner data for the list of specified ports. This
103
+ # stream provides a filtered, bandwidth-saving view of the
104
+ # Banners stream in case you are only interested in a
95
105
  # specific list of ports.
96
106
  # == Example
97
- # api.banners_on_port(80, 443) do |data|
107
+ # api.banners_on_port(80, 443) do |data|
98
108
  # # do something with the banner hash
99
109
  # puts data
100
110
  # end
101
111
  def banners_on_ports(*params)
102
- slurp_stream("shodan/ports/#{params.join(",")}") do |data|
112
+ slurp_stream("shodan/ports/#{params.join(',')}") do |data|
103
113
  yield data
104
114
  end
105
115
  end
106
116
 
107
- # Only returns banner data for a specific port. This
108
- # stream provides a filtered, bandwidth-saving view of the
109
- # Banners stream in case you are only interested in a
117
+ # Only returns banner data for a specific port. This
118
+ # stream provides a filtered, bandwidth-saving view of the
119
+ # Banners stream in case you are only interested in a
110
120
  # specific list of ports.
111
121
  # == Example
112
122
  # api.banners_on_port(80) do |banner|
@@ -120,10 +130,10 @@ module Shodanz
120
130
  end
121
131
 
122
132
  # Subscribe to banners discovered on all IP ranges described in the network alerts.
123
- # Use the REST API methods to create/ delete/ manage your network alerts and
133
+ # Use the REST API methods to create/ delete/ manage your network alerts and
124
134
  # use the Streaming API to subscribe to them.
125
135
  def alerts
126
- slurp_stream("alert") do |data|
136
+ slurp_stream('alert') do |data|
127
137
  yield data
128
138
  end
129
139
  end
@@ -134,54 +144,6 @@ module Shodanz
134
144
  yield data
135
145
  end
136
146
  end
137
-
138
- private
139
-
140
- # Perform the main function of consuming the streaming API.
141
- def slurp_stream(path, **params)
142
- uri = URI("#{URL}#{path}?key=#{@key}")
143
- Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
144
- request = Net::HTTP::Get.new uri
145
- begin
146
- http.request request do |resp|
147
- raise "Unable to connect to Streaming API" if resp.code != "200"
148
- # Buffer for Shodan's bullshit.
149
- raw_body = ""
150
- resp.read_body do |chunk|
151
- if /^\{"product":.*\}\}\n/.match(chunk)
152
- begin
153
- yield Oj.load(chunk)
154
- rescue
155
- # yolo
156
- end
157
- elsif /.*\}\}\n$/.match(chunk)
158
- next if raw_body.empty?
159
- raw_body << chunk
160
- raw_body
161
- elsif /^\{.*\b/.match(chunk)
162
- raw_body << chunk
163
- end
164
- if m = /^\{"product":.*\}\}\n/.match(raw_body)
165
- index = 0
166
- while matched = m[index]
167
- index += 1
168
- raw_body = raw_body.gsub(/^\{"product":.*\}\}\n/, "")
169
- begin
170
- yield Oj.load(matched)
171
- rescue
172
- # yolo
173
- end
174
- end
175
- end
176
- end
177
- end
178
- ensure
179
- http.finish
180
- end
181
- end
182
- end
183
-
184
147
  end
185
-
186
148
  end
187
149
  end