shodanz 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7a5b350b66032bb9e169a2adf65271c7841d8e0f
4
+ data.tar.gz: 2b57aabeeec1acf297a96e171710f15a0758dbd1
5
+ SHA512:
6
+ metadata.gz: d580381f2f606eb7c59ba1ee9019086c526d0f4a750de8317ecf8e2b3d1098201c52fffc30e6543adec32c4dc9d96b880d0b0af7315ddb47cf5b54f791fae68d
7
+ data.tar.gz: 25d2ffa6642f68d92a394be99fa16f63668777b13e9eb8198f21190443237791251eee5e0b4f3cd7bd6d8df1b8cc99bc54c76b42b30bbdaebf0579586d28f170
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.15.3
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at kgruber1@emich.edu. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in shodanz.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Kent Gruber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # Shodanz
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.
4
+
5
+ ## Installation
6
+
7
+ $ gem install shodanz
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ require "shodanz"
13
+
14
+ client = Shodanz.client.new
15
+ ```
16
+
17
+ ## REST API
18
+
19
+ The REST API provides methods to search Shodan, look up hosts, get summary information on queries and a variety of utility methods to make developing easier. Refer to the [REST API](https://developer.shodan.io/api) documentation for more ideas on how to use it.
20
+
21
+ ### Shodan Search Methods
22
+
23
+ Search'n for stuff, are 'ya?
24
+
25
+ #### Host Information
26
+
27
+ Returns all services that have been found on the given host IP.
28
+
29
+ ```ruby
30
+ client.rest_api.host("8.8.8.8") # Default
31
+ client.rest_api.host("8.8.8.8", history: true) # All historical banners should be returned.
32
+ client.rest_api.host("8.8.8.8", minify: true) # Only return the list of ports and the general host information, no banners.
33
+ ```
34
+
35
+ #### Host Search
36
+
37
+ Search Shodan using the same query syntax as the website and use facets to get summary information for different properties.
38
+
39
+ ```ruby
40
+ client.rest_api.host_search("mongodb")
41
+ client.rest_api.host_search("nginx")
42
+ client.rest_api.host_search("apache", after: "1/12/16")
43
+ client.rest_api.host_search("ssh", port: 22, page: 1)
44
+ client.rest_api.host_search("ssh", port: 22, page: 2)
45
+ client.rest_api.host_search("ftp", port: 21, facets: { link: "Ethernet or modem" })
46
+ ```
47
+
48
+ #### Search Shodan without Results
49
+
50
+ 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.
51
+
52
+ ```ruby
53
+ client.rest_api.host_count("apache")
54
+ client.rest_api.host_count("apache", country: "US")
55
+ client.rest_api.host_count("apache", country: "US", state: "MI")
56
+ client.rest_api.host_count("apache", country: "US", state: "MI", city: "Detroit")
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 })
60
+ ```
61
+
62
+ #### Scan Targets
63
+
64
+ Use this method to request Shodan to crawl an IP or netblock.
65
+
66
+ ```ruby
67
+ client.rest_api.scan("8.8.8.8")
68
+ ```
69
+
70
+ #### Crawl Internet for Port
71
+
72
+ Use this method to request Shodan to crawl the Internet for a specific port.
73
+
74
+ 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
+
76
+ ```ruby
77
+ client.rest_api.crawl_for(port: 80, protocol: "http")
78
+ ```
79
+
80
+ #### List Community Queries
81
+
82
+ Use this method to obtain a list of search queries that users have saved in Shodan.
83
+
84
+ ```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")
91
+ ```
92
+
93
+ #### Search Community Queries
94
+
95
+ Use this method to search the directory of search queries that users have saved in Shodan.
96
+
97
+ ```ruby
98
+ client.rest_api.search_for_community_query("the best")
99
+ client.rest_api.search_for_community_query("the best", page: 2)
100
+ ```
101
+
102
+ #### Popular Community Query Tags
103
+
104
+ Use this method to obtain a list of popular tags for the saved search queries in Shodan.
105
+
106
+ ```ruby
107
+ client.rest_api.popular_query_tags
108
+ client.rest_api.popular_query_tags(20)
109
+ ```
110
+
111
+ #### Protocols
112
+
113
+ This method returns an object containing all the protocols that can be used when launching an Internet scan.
114
+
115
+ ```ruby
116
+ client.rest_api.protocols
117
+ ```
118
+
119
+ #### Ports
120
+
121
+ This method returns a list of port numbers that the Shodan crawlers are looking for.
122
+
123
+ ```ruby
124
+ client.rest_api.ports
125
+ ```
126
+
127
+ #### Account Profile
128
+
129
+ Returns information about the Shodan account linked to this API key.
130
+
131
+ ```ruby
132
+ client.rest_api.profile
133
+ ```
134
+
135
+ #### DNS Lookup
136
+
137
+ Look up the IP address for the provided list of hostnames.
138
+
139
+ ```ruby
140
+ client.rest_api.resolve("google.com")
141
+ client.rest_api.resolve("google.com", "bing.com")
142
+ ```
143
+
144
+ #### Reverse DNS Lookup
145
+
146
+ Look up the hostnames that have been defined for the given list of IP addresses.
147
+
148
+ ```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")
151
+ ```
152
+
153
+ #### HTTP Headers
154
+
155
+ Shows the HTTP headers that your client sends when connecting to a webserver.
156
+
157
+ ```ruby
158
+ client.rest_api.http_headers
159
+ ```
160
+
161
+ #### Your IP Address
162
+
163
+ Get your current IP address as seen from the Internet.
164
+
165
+ ```ruby
166
+ client.rest_api.my_ip
167
+ ```
168
+
169
+ #### Honeypot Score
170
+
171
+ Calculates a honeypot probability score ranging from 0 (not a honeypot) to 1.0 (is a honeypot).
172
+
173
+ ```ruby
174
+ client.rest_api.honeypot_score('8.8.8.8')
175
+ ```
176
+
177
+ #### API Plan Information
178
+
179
+ ```ruby
180
+ client.rest_api.info
181
+ ```
182
+
183
+ ### Streaming API
184
+
185
+ The Streaming API is an HTTP-based service that returns a real-time stream of data collected by Shodan. Refer to the [Streaming API](https://developer.shodan.io/api/stream) documentation for more ideas on how to use it.
186
+ #### Banners
187
+
188
+ 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
+
190
+ ```ruby
191
+ client.streaming_api.banners do |data|
192
+ # do something with banner data
193
+ puts data
194
+ end
195
+ ```
196
+
197
+ #### Banners Filtered by ASN
198
+
199
+ 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
+
201
+ ```ruby
202
+ client.streaming_api.banners_within_asns(3303, 32475) do |data|
203
+ # do something with banner data
204
+ puts data
205
+ end
206
+ ```
207
+
208
+ #### Banners Filtered by Country
209
+
210
+ 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
+
212
+ ```ruby
213
+ client.streaming_api.banners_within_countries("DE", "US", "JP") do |data|
214
+ # do something with banner data
215
+ puts data
216
+ end
217
+ ```
218
+
219
+ #### Banners Filtered by Ports
220
+
221
+ 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
+
223
+ ```ruby
224
+ client.streaming_api.banners_on_ports(21, 22, 80) do |data|
225
+ # do something with banner data
226
+ puts data
227
+ end
228
+ ```
229
+
230
+ #### Banners by Network Alerts
231
+
232
+ Subscribe to banners discovered on all IP ranges described in the network alerts.
233
+
234
+ ```ruby
235
+ client.streaming_api.alerts do |data|
236
+ # do something with banner data
237
+ puts data
238
+ end
239
+ ```
240
+
241
+ #### Banner Filtered by Alert ID
242
+
243
+ Subscribe to banners discovered on the IP range defined in a specific network alert.
244
+
245
+ ```ruby
246
+ client.streaming_api.alert("HKVGAIRWD79Z7W2T") do |data|
247
+ # do something with banner data
248
+ puts data
249
+ end
250
+ ```
251
+
252
+ ### Exploits API
253
+
254
+ The Exploits API provides access to several exploit/ vulnerability data sources. Refer to the [Exploits API](https://developer.shodan.io/api/exploits/rest) documentation for more ideas on how to use it.
255
+
256
+ #### Search
257
+
258
+ Search across a variety of data sources for exploits and use facets to get summary information.
259
+
260
+ ```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.
265
+ ```
266
+
267
+ #### Count
268
+
269
+ This method behaves identical to the Exploits API `search` method with the difference that it doesn't return any results.
270
+
271
+ ```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.search(type: "shellcode") # A category of exploit to search for.
275
+ client.exploits_api.count(osvdb: "100007") # Open Source Vulnerability Database ID for the exploit.
276
+ ```
277
+
278
+ ## License
279
+
280
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/examples/debug.rb ADDED
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'shodanz'
3
+ require 'pry'
4
+
5
+ rest_api = Shodanz.api.rest.new
6
+ streaming_api = Shodanz.api.streaming.new
7
+ exploits_api = Shodanz.api.exploits.new
8
+
9
+ binding.pry
@@ -0,0 +1,66 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'shodanz'
3
+ require 'command_lion'
4
+ require 'yaml'
5
+ require 'pry'
6
+
7
+ module Top10
8
+
9
+ @rest_api = Shodanz.api.rest.new
10
+
11
+ def self.check(product)
12
+ begin
13
+ @rest_api.host_count(product: product, facets: { country: 10 })["facets"]["country"].collect { |x| x.values }.to_h.invert
14
+ rescue
15
+ puts "Unable to succesffully check the Shodan API."
16
+ exit 1
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ CommandLion::App.run do
23
+ name "Top 10 Countires Running a Product Using Shodan"
24
+
25
+ command :product do
26
+ description "Search for this given product."
27
+ type :string
28
+ flag "--product"
29
+
30
+ # Check is Shodan Enviroemnt Variable Set
31
+ before do
32
+ unless ENV['SHODAN_API_KEY']
33
+ puts "Need to set the 'SHODAN_API_KEY' enviroment variable before using this app!"
34
+ exit 1 # [ ╯´・ω・]╯︵┸━┸)
35
+ end
36
+ if argument.empty?
37
+ puts "What kind of nonsense is this?! You need to provide some argument..."
38
+ exit 1 # [ ╯ ゚▽゚]╯︵┻━┻)
39
+ end
40
+ end
41
+
42
+ # Do stuff.
43
+ action do
44
+ result = Top10.check(argument)
45
+ if options[:json].given?
46
+ puts JSON.pretty_generate(result)
47
+ elsif options[:yaml].given?
48
+ puts result.to_yaml
49
+ else
50
+ result.each do |country, count|
51
+ puts "#{country}\t#{count}"
52
+ end
53
+ end
54
+ end
55
+
56
+ option :json do
57
+ description "Use JSON as the format to output to STDOUT."
58
+ flag "--json"
59
+ end
60
+
61
+ option :yaml do
62
+ description "Use YAML as the format to output to STDOUT."
63
+ flag "--yaml"
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,32 @@
1
+ require_relative "apis/rest.rb"
2
+ require_relative "apis/streaming.rb"
3
+ require_relative "apis/exploits.rb"
4
+
5
+ module Shodanz
6
+ # There are 2 APIs for accessing Shodan: the REST API
7
+ # and the Streaming API. The REST API provides methods
8
+ # to search Shodan, look up hosts, get summary information
9
+ # on queries and a variety of utility methods to make
10
+ # developing easier. The Streaming API provides a raw,
11
+ # real-time feed of the data that Shodan is currently
12
+ # collecting. There are several feeds that can be subscribed
13
+ # to, but the data can't be searched or otherwise interacted
14
+ # with; it's a live feed of data meant for large-scale
15
+ # consumption of Shodan's information.
16
+ module API
17
+ # REST API class.
18
+ def self.rest
19
+ REST
20
+ end
21
+
22
+ # Streaming API class.
23
+ def self.streaming
24
+ Streaming
25
+ end
26
+
27
+ # Exploits API class.
28
+ def self.exploits
29
+ Exploits
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,85 @@
1
+ module Shodanz
2
+
3
+ module API
4
+ # The Exploits API provides access to several exploit
5
+ # and vulnerability data sources. At the moment, it
6
+ # searches across the following:
7
+ # - Exploit DB
8
+ # - Metasploit
9
+ # - Common Vulnerabilities and Exposures (CVE)
10
+ #
11
+ # @author Kent 'picat' Gruber
12
+ class Exploits
13
+ attr_accessor :key
14
+
15
+ # The path to the REST API endpoint.
16
+ URL = "https://exploits.shodan.io/api/"
17
+
18
+ # @param key [String] SHODAN API key, defaulted to the *SHODAN_API_KEY* enviroment variable.
19
+ def initialize(key: ENV['SHODAN_API_KEY'])
20
+ self.key = key
21
+ warn "No key has been found or provided!" unless self.key?
22
+ end
23
+
24
+ # Check if there's an API key.
25
+ def key?
26
+ return true if @key; false
27
+ end
28
+
29
+ # Search across a variety of data sources for exploits and
30
+ # use facets to get summary information.
31
+ # == Example
32
+ # api.search("SQL", port: 443)
33
+ # api.search(port: 22)
34
+ # api.search(type: "dos")
35
+ def search(query = "", facets: {}, page: 1, **params)
36
+ params[:query] = query
37
+ params = turn_into_query(params)
38
+ facets = turn_into_facets(facets)
39
+ params[:page] = page
40
+ get("search", params.merge(facets))
41
+ end
42
+
43
+ # This method behaves identical to the "/search" method with
44
+ # the difference that it doesn't return any results.
45
+ # == Example
46
+ # api.count(type: "dos")
47
+ def count(query = "", facets: {}, page: 1, **params)
48
+ params[:query] = query
49
+ params = turn_into_query(params)
50
+ facets = turn_into_facets(params)
51
+ params[:page] = page
52
+ get("count", params.merge(facets))
53
+ end
54
+
55
+ # Perform a direct GET HTTP request to the REST API.
56
+ def get(path, **params)
57
+ resp = Unirest.get "#{URL}#{path}?key=#{@key}", parameters: params
58
+ raise resp.body["error"] if resp.code != 200 and resp.body.has_key?("error")
59
+ resp.body
60
+ end
61
+
62
+ private
63
+
64
+ def turn_into_query(params)
65
+ filters = params.reject { |key, value| key == :query }
66
+ filters.each do |key, value|
67
+ params[:query] << " #{key}:#{value}"
68
+ end
69
+ params.select { |key, value| key == :query }
70
+ end
71
+
72
+ def turn_into_facets(facets)
73
+ filters = facets.reject { |key, value| key == :facets }
74
+ facets[:facets] = []
75
+ filters.each do |key, value|
76
+ facets[:facets] << "#{key}:#{value}"
77
+ end
78
+ facets[:facets] = facets[:facets].join(",")
79
+ facets.select { |key, value| key == :facets }
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,214 @@
1
+ module Shodanz
2
+
3
+ module API
4
+ # The REST API provides methods to search Shodan, look up
5
+ # hosts, get summary information on queries and a variety
6
+ # of other utilities. This requires you to have an API key
7
+ # which you can get from Shodan.
8
+ # @author Kent 'picat' Gruber
9
+ class REST
10
+ attr_accessor :key
11
+
12
+ # The path to the REST API endpoint.
13
+ URL = "https://api.shodan.io/"
14
+
15
+ # @param key [String] SHODAN API key, defaulted to the *SHODAN_API_KEY* enviroment variable.
16
+ def initialize(key: ENV['SHODAN_API_KEY'])
17
+ self.key = key
18
+ warn "No key has been found or provided!" unless self.key?
19
+ end
20
+
21
+ # Check if there's an API key.
22
+ def key?
23
+ return true if @key; false
24
+ end
25
+
26
+ # Returns all services that have been found on the given host IP.
27
+ # @param ip [String]
28
+ # @option params [Hash]
29
+ # @return [Hash]
30
+ # == Examples
31
+ # # Typical usage.
32
+ # rest_api.host("8.8.8.8")
33
+ #
34
+ # # All historical banners should be returned.
35
+ # rest_api.host("8.8.8.8", history: true)
36
+ #
37
+ # # Only return the list of ports and the general host information, no banners.
38
+ # rest_api.host("8.8.8.8", minify: true)
39
+ def host(ip, **params)
40
+ get("shodan/host/#{ip}", params)
41
+ end
42
+
43
+ # This method behaves identical to "/shodan/host/search" with the only
44
+ # difference that this method does not return any host results, it only
45
+ # returns the total number of results that matched the query and any
46
+ # facet information that was requested. As a result this method does
47
+ # not consume query credits.
48
+ # == Examples
49
+ # rest_api.host_count("apache")
50
+ # rest_api.host_count("apache", country: "US")
51
+ # rest_api.host_count("apache", country: "US", state: "MI")
52
+ # rest_api.host_count("apache", country: "US", state: "MI", city: "Detroit")
53
+ def host_count(query = "", facets: {}, **params)
54
+ params[:query] = query
55
+ params = turn_into_query(params)
56
+ facets = turn_into_facets(facets)
57
+ get("shodan/host/count", params.merge(facets))
58
+ end
59
+
60
+ # Search Shodan using the same query syntax as the website and use facets
61
+ # to get summary information for different properties.
62
+ # == Example
63
+ # rest_api.host_search("apache", country: "US", facets: { city: "Detroit" }, page: 1, minify: false)
64
+ def host_search(query = "", facets: {}, page: 1, minify: true, **params)
65
+ params[:query] = query
66
+ params = turn_into_query(params)
67
+ facets = turn_into_facets(facets)
68
+ params[:page] = page
69
+ params[:minify] = minify
70
+ get("shodan/host/search", params.merge(facets))
71
+ end
72
+
73
+ # This method lets you determine which filters are being used by
74
+ # the query string and what parameters were provided to the filters.
75
+ def host_search_tokens(query = "", **params)
76
+ params[:query] = query
77
+ params = turn_into_query(params)
78
+ get("shodan/host/search/tokens", params)
79
+ end
80
+
81
+ # This method returns a list of port numbers that the crawlers are looking for.
82
+ def ports
83
+ get("shodan/ports")
84
+ end
85
+
86
+ # List all protocols that can be used when performing on-demand Internet scans via Shodan.
87
+ def protocols
88
+ get("shodan/protocols")
89
+ end
90
+
91
+ # Use this method to request Shodan to crawl a network.
92
+ #
93
+ # This method uses API scan credits: 1 IP consumes 1 scan credit. You
94
+ # must have a paid API plan (either one-time payment or subscription)
95
+ # in order to use this method.
96
+ #
97
+ # IP, IPs or netblocks (in CIDR notation) that should get crawled.
98
+ def scan(*ips)
99
+ raise "Not enough scan credits!" unless self.info["scan_credits"] >= 1
100
+ post("shodan/scan", ips: ips.join(","))
101
+ end
102
+
103
+ # Use this method to request Shodan to crawl the Internet for a specific port.
104
+ #
105
+ # This method is restricted to security researchers and companies with
106
+ # a Shodan Data license. To apply for access to this method as a researcher,
107
+ # please email jmath@shodan.io with information about your project.
108
+ # Access is restricted to prevent abuse.
109
+ #
110
+ # == Example
111
+ # rest_api.crawl_for(port: 80, protocol: "http")
112
+ def crawl_for(**params)
113
+ params[:query] = ""
114
+ params = turn_into_query(params)
115
+ post("shodan/scan/internet", params)
116
+ end
117
+
118
+ # Check the progress of a previously submitted scan request.
119
+ def scan_status(id)
120
+ get("shodan/scan/#{id}")
121
+ end
122
+
123
+ # Use this method to obtain a list of search queries that users have saved in Shodan.
124
+ def community_queries(**params)
125
+ get("shodan/query", params)
126
+ end
127
+
128
+ # Use this method to search the directory of search queries that users have saved in Shodan.
129
+ def search_for_community_query(query, **params)
130
+ params[:query] = query
131
+ params = turn_into_query(params)
132
+ get("shodan/query/search", params)
133
+ end
134
+
135
+ # Use this method to obtain a list of popular tags for the saved search queries in Shodan.
136
+ def popular_query_tags(size = 10)
137
+ params = {}
138
+ params[:size] = size
139
+ get("shodan/query/tags", params)
140
+ end
141
+
142
+ # Returns information about the Shodan account linked to this API key.
143
+ def profile
144
+ get("account/profile")
145
+ end
146
+
147
+ # Look up the IP address for the provided list of hostnames.
148
+ def resolve(*hostnames)
149
+ get("dns/resolve", hostnames: hostnames.join(","))
150
+ end
151
+
152
+ # Look up the hostnames that have been defined for the given list of IP addresses.
153
+ def reverse_lookup(*ips)
154
+ get("dns/reverse", ips: ips.join(","))
155
+ end
156
+
157
+ # Shows the HTTP headers that your client sends when connecting to a webserver.
158
+ def http_headers
159
+ get("tools/httpheaders")
160
+ end
161
+
162
+ # Get your current IP address as seen from the Internet.
163
+ def my_ip
164
+ get("tools/my_ip")
165
+ end
166
+
167
+ # Calculates a honeypot probability score ranging from 0 (not a honeypot) to 1.0 (is a honeypot).
168
+ def honeypot_score(ip)
169
+ get("labs/honeyscore/#{ip}")
170
+ end
171
+
172
+ # Returns information about the API plan belonging to the given API key.
173
+ def info
174
+ get('api-info')
175
+ end
176
+
177
+ # Perform a direct GET HTTP request to the REST API.
178
+ def get(path, **params)
179
+ resp = Unirest.get "#{URL}#{path}?key=#{@key}", parameters: params
180
+ raise resp if resp.code != 200 #and resp.body.has_key?("error")
181
+ resp.body
182
+ end
183
+
184
+ # Perform a direct POST HTTP request to the REST API.
185
+ def post(path, **params)
186
+ resp = Unirest.post "#{URL}#{path}?key=#{@key}", parameters: params
187
+ raise resp.body["error"] if resp.code != 200 and resp.body.has_key?("error")
188
+ resp.body
189
+ end
190
+
191
+ private
192
+
193
+ def turn_into_query(params)
194
+ filters = params.reject { |key, value| key == :query }
195
+ filters.each do |key, value|
196
+ params[:query] << " #{key}:#{value}"
197
+ end
198
+ params.select { |key, value| key == :query }
199
+ end
200
+
201
+ def turn_into_facets(facets)
202
+ filters = facets.reject { |key, value| key == :facets }
203
+ facets[:facets] = []
204
+ filters.each do |key, value|
205
+ facets[:facets] << "#{key}:#{value}"
206
+ end
207
+ facets[:facets] = facets[:facets].join(",")
208
+ facets.select { |key, value| key == :facets }
209
+ end
210
+
211
+ end
212
+
213
+ end
214
+ end
@@ -0,0 +1,187 @@
1
+ module Shodanz
2
+
3
+ module API
4
+ # The REST API provides methods to search Shodan, look up
5
+ # hosts, get summary information on queries and a variety
6
+ # of other utilities. This requires you to have an API key
7
+ # which you can get from Shodan.
8
+ #
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
+ # please contact us for pricing information (sales@shodan.io).
13
+ #
14
+ # @author Kent 'picat' Gruber
15
+ class Streaming
16
+ attr_accessor :key
17
+
18
+ # The Streaming API is an HTTP-based service that returns
19
+ # a real-time stream of data collected by Shodan.
20
+ URL = "https://stream.shodan.io/"
21
+
22
+ # @param key [String] SHODAN API key, defaulted to the *SHODAN_API_KEY* enviroment variable.
23
+ def initialize(key: ENV['SHODAN_API_KEY'])
24
+ self.key = key
25
+ warn "No key has been found or provided!" unless self.key?
26
+ end
27
+
28
+ # Check if there's an API key.
29
+ def key?
30
+ return true if @key; false
31
+ end
32
+
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
36
+ # ports, please use the Ports stream.
37
+ #
38
+ # Sometimes data may be piped down stream that is weird to parse. You can choose
39
+ # to keep this data optionally; and it will not be parsed for you.
40
+ #
41
+ # == Example
42
+ # api.banners do |banner|
43
+ # # do something with banner as hash
44
+ # puts data
45
+ # end
46
+ def banners(**params)
47
+ slurp_stream("shodan/banners", params) do |data|
48
+ yield data
49
+ end
50
+ end
51
+
52
+ # This stream provides a filtered, bandwidth-saving view of the Banners
53
+ # stream in case you are only interested in devices located in certain ASNs.
54
+ # == Example
55
+ # api.banners_within_asns(3303, 32475) do |data|
56
+ # # do something with the banner hash
57
+ # puts data
58
+ # end
59
+ def banners_within_asns(*asns, **params)
60
+ slurp_stream("shodan/asn/#{asns.join(",")}", params) do |data|
61
+ yield data
62
+ end
63
+ end
64
+
65
+ # This stream provides a filtered, bandwidth-saving view of the Banners
66
+ # stream in case you are only interested in devices located in a certain ASN.
67
+ # == Example
68
+ # api.banners_within_asn(3303) do |data|
69
+ # # do something with the banner hash
70
+ # puts data
71
+ # end
72
+ def banners_within_asn(param)
73
+ banners_within_asns(param) do |data|
74
+ yield data
75
+ end
76
+ end
77
+
78
+ # Only returns banner data for the list of specified ports. This
79
+ # stream provides a filtered, bandwidth-saving view of the Banners
80
+ # stream in case you are only interested in a specific list of ports.
81
+ # == Example
82
+ # api.banners_within_countries("US","DE","JP") do |data|
83
+ # # do something with the banner hash
84
+ # puts data
85
+ # end
86
+ def banners_within_countries(*params)
87
+ slurp_stream("shodan/countries/#{params.join(",")}") do |data|
88
+ yield data
89
+ end
90
+ end
91
+
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
95
+ # specific list of ports.
96
+ # == Example
97
+ # api.banners_on_port(80, 443) do |data|
98
+ # # do something with the banner hash
99
+ # puts data
100
+ # end
101
+ def banners_on_ports(*params)
102
+ slurp_stream("shodan/ports/#{params.join(",")}") do |data|
103
+ yield data
104
+ end
105
+ end
106
+
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
110
+ # specific list of ports.
111
+ # == Example
112
+ # api.banners_on_port(80) do |banner|
113
+ # # do something with the banner hash
114
+ # puts data
115
+ # end
116
+ def banners_on_port(param)
117
+ banners_on_ports(param) do |data|
118
+ yield data
119
+ end
120
+ end
121
+
122
+ # 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
124
+ # use the Streaming API to subscribe to them.
125
+ def alerts
126
+ slurp_stream("alert") do |data|
127
+ yield data
128
+ end
129
+ end
130
+
131
+ # Subscribe to banners discovered on the IP range defined in a specific network alert.
132
+ def alert(id)
133
+ slurp_stream("alert/#{id}") do |data|
134
+ yield data
135
+ end
136
+ 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
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,22 @@
1
+ module Shodanz
2
+ class Client
3
+ # Create a new client to connect to any of the APIs.
4
+ def initialize
5
+ @rest_api = Shodanz.api.rest.new
6
+ @streaming_api = Shodanz.api.streaming.new
7
+ @exploits_api = Shodanz.api.exploits.new
8
+ end
9
+
10
+ def rest_api
11
+ @rest_api
12
+ end
13
+
14
+ def streaming_api
15
+ @streaming_api
16
+ end
17
+
18
+ def exploits_api
19
+ @exploits_api
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Shodanz
2
+ VERSION = "1.0.0"
3
+ end
data/lib/shodanz.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "oj"
2
+ require "unirest"
3
+ require "shodanz/version"
4
+ require "shodanz/api"
5
+ require "shodanz/client"
6
+
7
+ module Shodanz
8
+ def self.api
9
+ API
10
+ end
11
+ def self.client
12
+ Client
13
+ end
14
+ end
data/shodanz.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "shodanz/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "shodanz"
8
+ spec.version = Shodanz::VERSION
9
+ spec.authors = ["Kent 'picatz' Gruber"]
10
+ spec.email = ["kgruber1@emich.edu"]
11
+
12
+ spec.summary = %q{A modern Ruby gem for Shodan, the world's first search engine for Internet-connected devices.}
13
+ spec.description = %q{Featuring full support for the REST, Streaming and Exploits API}
14
+ spec.homepage = "https://github.com/picatz/shodanz"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "unirest"
23
+ spec.add_dependency "oj"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.15"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shodanz
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kent 'picatz' Gruber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: unirest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.15'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ description: Featuring full support for the REST, Streaming and Exploits API
84
+ email:
85
+ - kgruber1@emich.edu
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".travis.yml"
93
+ - CODE_OF_CONDUCT.md
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - examples/debug.rb
99
+ - examples/top_10_countries_running.rb
100
+ - lib/shodanz.rb
101
+ - lib/shodanz/api.rb
102
+ - lib/shodanz/apis/exploits.rb
103
+ - lib/shodanz/apis/rest.rb
104
+ - lib/shodanz/apis/streaming.rb
105
+ - lib/shodanz/client.rb
106
+ - lib/shodanz/version.rb
107
+ - shodanz.gemspec
108
+ homepage: https://github.com/picatz/shodanz
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.6.12
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: A modern Ruby gem for Shodan, the world's first search engine for Internet-connected
132
+ devices.
133
+ test_files: []