shodanz 1.0.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30b41a97e4733cbc6a78b283019eb135fe36114768f7e032b6bc6df9f317e051
4
- data.tar.gz: 7d4bc7144c8dad6dd37b7df896fa8cef8248c132f640d81ea86ae5dc4d5631c5
3
+ metadata.gz: edfee7e174978325331ceb16d7d7f19a1cd7f7cdb6731269cf8f3bc9d9fdcb31
4
+ data.tar.gz: d89a612b5e3e90aa0ef9a288cf9650875f9a0031bf936d94b82d45852fb887ba
5
5
  SHA512:
6
- metadata.gz: 5ac1a82fc5b214b7b0e9f9add4f381d1aa52618dfbdc0ff1b8005417ff93752f8e48b1e2b0ed146893bc6db9bc04d56c5df8b6c104d3207ca4df28d19e6ba1ca
7
- data.tar.gz: fa645b5ce29c8fc267a6398127b8c2a0fb67937a67d38a92d686c669175d39fd53c9c55db3546d07d655fadd108914bcdf381710dda5b62a3d6c26e777b43248
6
+ metadata.gz: 3f9e3e19895688ad861517883165a0b06b965f89c28854d73bec521b7a0430ebd5394519aaf4fef5e5397c9c31520bff32eed9cbeed336f4c3932bf34b1c4f09
7
+ data.tar.gz: '09326a94bb100c9abe45715fc0430f4f8dedbfffbba359346b268d17b4c8523fbf3795c93cd054ed4b68af98e1166857385c8a6df5b66278d0fea56164bea6cc'
data/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Shodanz
2
2
 
3
- A modern Ruby [gem](https://rubygems.org/) for [Shodan](https://www.shodan.io/), the world's first search engine for Internet-connected devices.
3
+ A modern, async Ruby [gem](https://rubygems.org/) for [Shodan](https://www.shodan.io/), the world's first search engine for Internet-connected devices.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  $ gem install shodanz
8
-
9
8
  ## Usage
10
9
 
11
10
  ```ruby
@@ -13,7 +12,37 @@ require "shodanz"
13
12
 
14
13
  client = Shodanz.client.new(key: "YOUR_API_KEY")
15
14
  ```
16
- > You can also set the `SHODAN_API_KEY` environment variable instead of passing the API key as an argument when creating a client.
15
+ > **NOTE:** You can also set the `SHODAN_API_KEY` environment variable instead of passing the API key as an argument when creating a client.
16
+
17
+ ### Optional Async Support
18
+
19
+ Shodanz utilizes [async](https://github.com/socketry/async) to provide asyncronous IO. This doesn't break any existing scripts using Shodanz, but now offers even more flexibility to write more awesome things, like this asyncronous honeypot detector:
20
+
21
+ ```ruby
22
+ require 'async'
23
+ require 'shodanz'
24
+
25
+ client = Shodanz.client.new
26
+
27
+ # Asyncronously stream live banner info from shodan
28
+ # and check the IP addresses against the expierimental
29
+ # honeypot scoring service to find potential honeypots.
30
+ client.streaming_api.banners do |banner|
31
+ if ip = banner['ip_str']
32
+ Async do
33
+ score = client.rest_api.honeypot_score(ip).wait
34
+ puts "#{ip} has a #{score * 100}% chance of being a honeypot"
35
+ rescue Shodanz::Errors::RateLimited
36
+ sleep rand
37
+ retry
38
+ rescue # any other errors
39
+ next
40
+ end
41
+ end
42
+ end
43
+ ```
44
+
45
+ > **Note:** To run any Shodanz method asyncronously, simply wrap it in a `Async { ... }` block. To wait for any other async operation to finnish in the block, call `.wait` on it.
17
46
 
18
47
  ## REST API
19
48
 
@@ -28,9 +57,9 @@ Search'n for stuff, are 'ya?
28
57
  Returns all services that have been found on the given host IP.
29
58
 
30
59
  ```ruby
31
- client.rest_api.host("8.8.8.8") # Default
32
- client.rest_api.host("8.8.8.8", history: true) # All historical banners should be returned.
33
- client.rest_api.host("8.8.8.8", minify: true) # Only return the list of ports and the general host information, no banners.
60
+ client.host("8.8.8.8") # Default
61
+ client.host("8.8.8.8", history: true) # All historical banners should be returned.
62
+ client.host("8.8.8.8", minify: true) # Only return the list of ports and the general host information, no banners.
34
63
  ```
35
64
 
36
65
  #### Host Search
@@ -38,12 +67,12 @@ client.rest_api.host("8.8.8.8", minify: true) # Only return the list of ports a
38
67
  Search Shodan using the same query syntax as the website and use facets to get summary information for different properties.
39
68
 
40
69
  ```ruby
41
- client.rest_api.host_search("mongodb")
42
- client.rest_api.host_search("nginx")
43
- client.rest_api.host_search("apache", after: "1/12/16")
44
- client.rest_api.host_search("ssh", port: 22, page: 1)
45
- client.rest_api.host_search("ssh", port: 22, page: 2)
46
- client.rest_api.host_search("ftp", port: 21, facets: { link: "Ethernet or modem" })
70
+ client.host_search("mongodb")
71
+ client.host_search("nginx")
72
+ client.host_search("apache", after: "1/12/16")
73
+ client.host_search("ssh", port: 22, page: 1)
74
+ client.host_search("ssh", port: 22, page: 2)
75
+ client.host_search("ftp", port: 21, facets: { link: "Ethernet or modem" })
47
76
  ```
48
77
 
49
78
  #### Search Shodan without Results
@@ -51,12 +80,12 @@ client.rest_api.host_search("ftp", port: 21, facets: { link: "Ethernet or modem"
51
80
  This method behaves identical to `host_search` with the only difference that this method does not return any host results, it only returns the total number of results that matched the query and any facet information that was requested. As a result this method does not consume query credits.
52
81
 
53
82
  ```ruby
54
- client.rest_api.host_count("apache")
55
- client.rest_api.host_count("apache", country: "US")
56
- client.rest_api.host_count("apache", country: "US", state: "MI")
57
- client.rest_api.host_count("apache", country: "US", state: "MI", city: "Detroit")
58
- client.rest_api.host_count("nginx", facets: { country: 5 })
59
- client.rest_api.host_count("apache", facets: { country: 5 })
83
+ client.host_count("apache")
84
+ client.host_count("apache", country: "US")
85
+ client.host_count("apache", country: "US", state: "MI")
86
+ client.host_count("apache", country: "US", state: "MI", city: "Detroit")
87
+ client.host_count("nginx", facets: { country: 5 })
88
+ client.host_count("apache", facets: { country: 5 })
60
89
  ```
61
90
 
62
91
  #### Scan Targets
@@ -64,7 +93,7 @@ client.rest_api.host_count("apache", facets: { country: 5 })
64
93
  Use this method to request Shodan to crawl an IP or netblock.
65
94
 
66
95
  ```ruby
67
- client.rest_api.scan("8.8.8.8")
96
+ client.scan("8.8.8.8")
68
97
  ```
69
98
 
70
99
  #### Crawl Internet for Port
@@ -74,7 +103,7 @@ Use this method to request Shodan to crawl the Internet for a specific port.
74
103
  This method is restricted to security researchers and companies with a Shodan Data license. To apply for access to this method as a researcher, please email `jmath@shodan.io` with information about your project. Access is restricted to prevent abuse.
75
104
 
76
105
  ```ruby
77
- client.rest_api.crawl_for(port: 80, protocol: "http")
106
+ client.crawl_for(port: 80, protocol: "http")
78
107
  ```
79
108
 
80
109
  #### List Community Queries
@@ -82,12 +111,12 @@ client.rest_api.crawl_for(port: 80, protocol: "http")
82
111
  Use this method to obtain a list of search queries that users have saved in Shodan.
83
112
 
84
113
  ```ruby
85
- client.rest_api.community_queries
86
- client.rest_api.community_queries(page: 2)
87
- client.rest_api.community_queries(sort: "votes")
88
- client.rest_api.community_queries(sort: "votes", page: 2)
89
- client.rest_api.community_queries(order: "asc")
90
- client.rest_api.community_queries(order: "desc")
114
+ client.community_queries
115
+ client.community_queries(page: 2)
116
+ client.community_queries(sort: "votes")
117
+ client.community_queries(sort: "votes", page: 2)
118
+ client.community_queries(order: "asc")
119
+ client.community_queries(order: "desc")
91
120
  ```
92
121
 
93
122
  #### Search Community Queries
@@ -95,8 +124,8 @@ client.rest_api.community_queries(order: "desc")
95
124
  Use this method to search the directory of search queries that users have saved in Shodan.
96
125
 
97
126
  ```ruby
98
- client.rest_api.search_for_community_query("the best")
99
- client.rest_api.search_for_community_query("the best", page: 2)
127
+ client.search_for_community_query("the best")
128
+ client.search_for_community_query("the best", page: 2)
100
129
  ```
101
130
 
102
131
  #### Popular Community Query Tags
@@ -104,8 +133,8 @@ client.rest_api.search_for_community_query("the best", page: 2)
104
133
  Use this method to obtain a list of popular tags for the saved search queries in Shodan.
105
134
 
106
135
  ```ruby
107
- client.rest_api.popular_query_tags
108
- client.rest_api.popular_query_tags(20)
136
+ client.popular_query_tags
137
+ client.popular_query_tags(20)
109
138
  ```
110
139
 
111
140
  #### Protocols
@@ -113,7 +142,7 @@ client.rest_api.popular_query_tags(20)
113
142
  This method returns an object containing all the protocols that can be used when launching an Internet scan.
114
143
 
115
144
  ```ruby
116
- client.rest_api.protocols
145
+ client.protocols
117
146
  ```
118
147
 
119
148
  #### Ports
@@ -121,7 +150,7 @@ client.rest_api.protocols
121
150
  This method returns a list of port numbers that the Shodan crawlers are looking for.
122
151
 
123
152
  ```ruby
124
- client.rest_api.ports
153
+ client.ports
125
154
  ```
126
155
 
127
156
  #### Account Profile
@@ -129,7 +158,7 @@ client.rest_api.ports
129
158
  Returns information about the Shodan account linked to this API key.
130
159
 
131
160
  ```ruby
132
- client.rest_api.profile
161
+ client.profile
133
162
  ```
134
163
 
135
164
  #### DNS Lookup
@@ -137,8 +166,8 @@ client.rest_api.profile
137
166
  Look up the IP address for the provided list of hostnames.
138
167
 
139
168
  ```ruby
140
- client.rest_api.resolve("google.com")
141
- client.rest_api.resolve("google.com", "bing.com")
169
+ client.resolve("google.com")
170
+ client.resolve("google.com", "bing.com")
142
171
  ```
143
172
 
144
173
  #### Reverse DNS Lookup
@@ -146,8 +175,8 @@ client.rest_api.resolve("google.com", "bing.com")
146
175
  Look up the hostnames that have been defined for the given list of IP addresses.
147
176
 
148
177
  ```ruby
149
- client.rest_api.reverse_lookup("74.125.227.230")
150
- client.rest_api.reverse_lookup("74.125.227.230", "204.79.197.200")
178
+ client.reverse_lookup("74.125.227.230")
179
+ client.reverse_lookup("74.125.227.230", "204.79.197.200")
151
180
  ```
152
181
 
153
182
  #### HTTP Headers
@@ -155,7 +184,7 @@ client.rest_api.reverse_lookup("74.125.227.230", "204.79.197.200")
155
184
  Shows the HTTP headers that your client sends when connecting to a webserver.
156
185
 
157
186
  ```ruby
158
- client.rest_api.http_headers
187
+ client.http_headers
159
188
  ```
160
189
 
161
190
  #### Your IP Address
@@ -163,7 +192,7 @@ client.rest_api.http_headers
163
192
  Get your current IP address as seen from the Internet.
164
193
 
165
194
  ```ruby
166
- client.rest_api.my_ip
195
+ client.my_ip
167
196
  ```
168
197
 
169
198
  #### Honeypot Score
@@ -171,13 +200,13 @@ client.rest_api.my_ip
171
200
  Calculates a honeypot probability score ranging from 0 (not a honeypot) to 1.0 (is a honeypot).
172
201
 
173
202
  ```ruby
174
- client.rest_api.honeypot_score('8.8.8.8')
203
+ client.honeypot_score('8.8.8.8')
175
204
  ```
176
205
 
177
206
  #### API Plan Information
178
207
 
179
208
  ```ruby
180
- client.rest_api.info
209
+ client.info
181
210
  ```
182
211
 
183
212
  ### Streaming API
@@ -188,7 +217,7 @@ The Streaming API is an HTTP-based service that returns a real-time stream of da
188
217
  This stream provides ALL of the data that Shodan collects. Use this stream if you need access to everything and/ or want to store your own Shodan database locally. If you only care about specific ports, please use the Ports stream.
189
218
 
190
219
  ```ruby
191
- client.streaming_api.banners do |data|
220
+ client.banners do |data|
192
221
  # do something with banner data
193
222
  puts data
194
223
  end
@@ -199,7 +228,7 @@ end
199
228
  This stream provides a filtered, bandwidth-saving view of the Banners stream in case you are only interested in devices located in certain ASNs.
200
229
 
201
230
  ```ruby
202
- client.streaming_api.banners_within_asns(3303, 32475) do |data|
231
+ client.banners_within_asns(3303, 32475) do |data|
203
232
  # do something with banner data
204
233
  puts data
205
234
  end
@@ -210,18 +239,18 @@ end
210
239
  This stream provides a filtered, bandwidth-saving view of the Banners stream in case you are only interested in devices located in certain countries.
211
240
 
212
241
  ```ruby
213
- client.streaming_api.banners_within_countries("DE", "US", "JP") do |data|
242
+ client.banners_within_countries("DE", "US", "JP") do |data|
214
243
  # do something with banner data
215
244
  puts data
216
245
  end
217
246
  ```
218
247
 
219
- #### Banners Filtered by Ports
248
+ #### Banners Filtered by Ports
220
249
 
221
250
  Only returns banner data for the list of specified ports. This stream provides a filtered, bandwidth-saving view of the Banners stream in case you are only interested in a specific list of ports.
222
251
 
223
252
  ```ruby
224
- client.streaming_api.banners_on_ports(21, 22, 80) do |data|
253
+ client.banners_on_ports(21, 22, 80) do |data|
225
254
  # do something with banner data
226
255
  puts data
227
256
  end
@@ -232,7 +261,7 @@ end
232
261
  Subscribe to banners discovered on all IP ranges described in the network alerts.
233
262
 
234
263
  ```ruby
235
- client.streaming_api.alerts do |data|
264
+ client.alerts do |data|
236
265
  # do something with banner data
237
266
  puts data
238
267
  end
@@ -243,7 +272,7 @@ end
243
272
  Subscribe to banners discovered on the IP range defined in a specific network alert.
244
273
 
245
274
  ```ruby
246
- client.streaming_api.alert("HKVGAIRWD79Z7W2T") do |data|
275
+ client.alert("HKVGAIRWD79Z7W2T") do |data|
247
276
  # do something with banner data
248
277
  puts data
249
278
  end
@@ -258,10 +287,10 @@ The Exploits API provides access to several exploit/ vulnerability data sources.
258
287
  Search across a variety of data sources for exploits and use facets to get summary information.
259
288
 
260
289
  ```ruby
261
- client.exploits_api.search("python") # Search for Snek vulns.
262
- client.exploits_api.search(post: 22) # Port number for the affected service if the exploit is remote.
263
- client.exploits_api.search(type: "shellcode") # A category of exploit to search for.
264
- client.exploits_api.search(osvdb: "100007") # Open Source Vulnerability Database ID for the exploit.
290
+ client.search("python") # Search for Snek vulns.
291
+ client.search(post: 22) # Port number for the affected service if the exploit is remote.
292
+ client.search(type: "shellcode") # A category of exploit to search for.
293
+ client.search(osvdb: "100007") # Open Source Vulnerability Database ID for the exploit.
265
294
  ```
266
295
 
267
296
  #### Count
@@ -269,10 +298,10 @@ client.exploits_api.search(osvdb: "100007") # Open Source Vulnerability Dat
269
298
  This method behaves identical to the Exploits API `search` method with the difference that it doesn't return any results.
270
299
 
271
300
  ```ruby
272
- client.exploits_api.count("python") # Count Snek vulns.
273
- client.exploits_api.count(port: 22) # Port number for the affected service if the exploit is remote.
274
- client.exploits_api.count(type: "shellcode") # A category of exploit to search for.
275
- client.exploits_api.count(osvdb: "100007") # Open Source Vulnerability Database ID for the exploit.
301
+ client.count("python") # Count Snek vulns.
302
+ client.count(port: 22) # Port number for the affected service if the exploit is remote.
303
+ client.count(type: "shellcode") # A category of exploit to search for.
304
+ client.count(osvdb: "100007") # Open Source Vulnerability Database ID for the exploit.
276
305
  ```
277
306
 
278
307
  ## License
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2
+ require 'async'
3
+ require 'shodanz'
4
+
5
+ client = Shodanz.client.new
6
+
7
+ client.streaming_api.banners do |banner|
8
+ if ip = banner['ip_str']
9
+ Async do
10
+ score = client.rest_api.honeypot_score(ip).wait
11
+ puts "#{ip} has a #{score * 100}% chance of being a honeypot"
12
+ rescue Shodanz::Errors::RateLimited
13
+ sleep rand
14
+ retry
15
+ rescue # any other errors
16
+ next
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'async'
3
+ require 'shodanz'
4
+
5
+ client = Shodanz.client.new
6
+
7
+ webservers = ['apache', 'nginx', 'caddy', 'lighttpd', 'cherokee']
8
+
9
+ # we can use methods sequentially
10
+ started = Time.now.sec
11
+ webservers.each do |webserver|
12
+ # make HTTP request
13
+ client.rest_api.host_search(webserver)
14
+ # print webserver to STDOUT
15
+ puts webserver
16
+ end
17
+ puts "Sequential took #{Time.now.sec - started} seconds"
18
+
19
+ # we can also use methods asyncronously
20
+ started = Time.now.sec
21
+ Async do
22
+ webservers.each do |webserver|
23
+ # make HTTP request
24
+ client.rest_api.host_search(webserver)
25
+ # print webserver to STDOUT
26
+ puts webserver
27
+ end
28
+ end
29
+ puts "Asyncronous took #{Time.now.sec - started} seconds"
@@ -0,0 +1,33 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'pry'
3
+ require 'async'
4
+ require 'shodanz'
5
+
6
+ client = Shodanz.client.new
7
+
8
+ stats = Hash.new(0)
9
+
10
+ ports = [21, 22, 80, 443]
11
+ services = ['ftp', 'ssh', 'http', 'https']
12
+
13
+ ports_with_service_names = ports.zip(services)
14
+
15
+ Async do
16
+ # collect banners for ports
17
+ ports_with_service_names.each do |port, service|
18
+ client.streaming_api.banners_on_port(port) do |banner|
19
+ if ip = banner['ip_str']
20
+ Async do
21
+ resp = client.rest_api.honeypot_score(ip).wait
22
+ binding.pry if resp.nil?
23
+ puts "#{ip} has a #{resp *100}% chance of being a honeypot"
24
+ rescue Shodanz::Errors::RateLimited
25
+ sleep 1
26
+ retry
27
+ rescue => error
28
+ binding.pry
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -15,5 +15,8 @@ stats = Hash.new(0)
15
15
  # collect banners
16
16
  streaming_api.banners do |banner|
17
17
  product = banner['product']
18
+
19
+ next if product.nil?
20
+
18
21
  puts "#{stats[product] += 1} #{product}"
19
22
  end
@@ -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/api/'
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
+ @internet = Async::HTTP::Internet.new
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
 
@@ -54,35 +64,6 @@ module Shodanz
54
64
  params[:page] = page
55
65
  get('count', params.merge(facets))
56
66
  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 }
85
- end
86
67
  end
87
68
  end
88
69
  end