shodan-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,250 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ require 'shodan/extensions/uri'
22
+ require 'shodan/countries'
23
+ require 'shodan/has_pages'
24
+ require 'shodan/page'
25
+ require 'shodan/shodan'
26
+
27
+ require 'net/http'
28
+ require 'nokogiri'
29
+
30
+ module Shodan
31
+ class Query
32
+
33
+ include HasPages
34
+
35
+ # Search URL
36
+ SEARCH_URL = 'http://shodan.surtri.com/'
37
+
38
+ # Results per page
39
+ RESULTS_PER_PAGE = 20
40
+
41
+ # Search query
42
+ attr_accessor :query
43
+
44
+ # Countries to search within
45
+ attr_accessor :countries
46
+
47
+ # Hostnames to search for
48
+ attr_accessor :hostnames
49
+
50
+ # CIDR Network blocks to search within
51
+ attr_accessor :networks
52
+
53
+ # Ports to search for
54
+ attr_accessor :ports
55
+
56
+ #
57
+ # Creates a new Query object.
58
+ #
59
+ # @param [Hash] options
60
+ # Additional options.
61
+ #
62
+ # @option options [String] :query
63
+ # The query expression.
64
+ #
65
+ # @option options [Array<String>] :countries
66
+ # The Country Codes to search within.
67
+ #
68
+ # @option options [String] :country
69
+ # A Country Code to search within.
70
+ #
71
+ # @option options [Array<String>] :hostnames
72
+ # The host names to search for.
73
+ #
74
+ # @option options [String] :hostname
75
+ # A host name to search for.
76
+ #
77
+ # @option options [Array<String>] :networks
78
+ # The CIDR network blocks to search within.
79
+ #
80
+ # @option options [String] :network
81
+ # A CIDR network blocks to search within.
82
+ #
83
+ # @option options [Array<Integer>] :ports
84
+ # The ports to search for.
85
+ #
86
+ # @option options [Integer] :port
87
+ # A port to search for.
88
+ #
89
+ # @yield [query]
90
+ # If a block is given, it will be passed the newly created Query
91
+ # object.
92
+ #
93
+ # @yieldparam [Query] query
94
+ # The newly created Query object.
95
+ #
96
+ def initialize(options={},&block)
97
+ @agent = Shodan.web_agent
98
+ @query = options[:query]
99
+
100
+ @countries = []
101
+
102
+ if options[:countries]
103
+ @countries += options[:countries]
104
+ elsif options[:country]
105
+ @countries << option[:country]
106
+ end
107
+
108
+ @hostnames = []
109
+
110
+ if options[:hostnames]
111
+ @hostnames += options[:hostnames]
112
+ elsif options[:hostname]
113
+ @hostnames << options[:hostname]
114
+ end
115
+
116
+ @networks = []
117
+
118
+ if options[:networks]
119
+ @networks += options[:networks]
120
+ elsif options[:network]
121
+ @networks << options[:network]
122
+ end
123
+
124
+ @ports = []
125
+
126
+ if options[:ports]
127
+ @ports += options[:ports]
128
+ elsif options[:port]
129
+ @ports << options[:port]
130
+ end
131
+
132
+ block.call(self) if block
133
+ end
134
+
135
+ #
136
+ # Converts a given URL into a Query object.
137
+ #
138
+ # @param [URI::HTTP, String] url
139
+ # The query URL.
140
+ #
141
+ # @return [Query]
142
+ # The Query object created from the URL.
143
+ #
144
+ def self.from_url(url)
145
+ url = URI(url.to_s)
146
+
147
+ return self.new(
148
+ :query => url.query_params['q'].gsub('+',' ')
149
+ )
150
+ end
151
+
152
+ #
153
+ # The results per page.
154
+ #
155
+ # @return [Integer]
156
+ # The resutls per page.
157
+ #
158
+ # @see RESULTS_PER_PAGE
159
+ #
160
+ def results_per_page
161
+ RESULTS_PER_PAGE
162
+ end
163
+
164
+ #
165
+ # The query expression from the query.
166
+ #
167
+ # @return [String]
168
+ # The query expression.
169
+ #
170
+ def expression
171
+ expr = []
172
+
173
+ expr << @query if @query
174
+
175
+ expr += @countries.map { |code| "country:#{code}" }
176
+ expr += @hostnames.map { |host| "hostname:#{host}" }
177
+ expr += @networks.map { |net| "net:#{net}" }
178
+ expr += @ports.map { |port| "port:#{port}" }
179
+
180
+ return expr.join(' ')
181
+ end
182
+
183
+ #
184
+ # The search URL for the query.
185
+ #
186
+ # @return [URI::HTTP]
187
+ # The search URL.
188
+ #
189
+ def search_url
190
+ url = URI(SEARCH_URL)
191
+
192
+ url.query_params['q'] = expression
193
+ return url
194
+ end
195
+
196
+ #
197
+ # The search URL for the query and given page index.
198
+ #
199
+ # @param [Integer] index
200
+ # The page index to lookup.
201
+ #
202
+ # @retrun [URI::HTTP]
203
+ # The search URL for the query and the specified page index.
204
+ #
205
+ def page_url(index)
206
+ url = search_url
207
+
208
+ unless index == 1
209
+ url.query_params['page'] = index
210
+ end
211
+
212
+ return url
213
+ end
214
+
215
+ #
216
+ # Requests a page at the given index.
217
+ #
218
+ # @param [Integer] index
219
+ # The index of the page.
220
+ #
221
+ # @return [Page]
222
+ # The page at the specified index.
223
+ #
224
+ def page(index)
225
+ Page.new do |new_page|
226
+ doc = @agent.get(page_url(index))
227
+
228
+ doc.search('#search/div.result').each do |result|
229
+ div = result.at('div')
230
+
231
+ ip = if (a = div.at('a'))
232
+ a.inner_text
233
+ else
234
+ div.children.first.inner_text
235
+ end
236
+
237
+ hostname = if (host_node = result.at('div/a:last'))
238
+ host_node.inner_text
239
+ end
240
+
241
+ date = result.at('div/span').inner_text.scan(/\d+\.\d+\.\d/).first
242
+ response = result.at('p').inner_text.strip
243
+
244
+ new_page << Host.new(ip,date,response,hostname)
245
+ end
246
+ end
247
+ end
248
+
249
+ end
250
+ end
@@ -0,0 +1,216 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ require 'shodan/query'
22
+
23
+ require 'mechanize'
24
+
25
+ module Shodan
26
+ # Common proxy port.
27
+ COMMON_PROXY_PORT = 8080
28
+
29
+ #
30
+ # The proxy information used by {Query}.
31
+ #
32
+ # @return [Hash]
33
+ # The proxy information.
34
+ #
35
+ def Shodan.proxy
36
+ @@shodan_proxy ||= {:host => nil, :port => COMMON_PROXY_PORT, :user => nil, :password => nil}
37
+ end
38
+
39
+ #
40
+ # Creates a HTTP URI based from the given proxy information.
41
+ #
42
+ # @param [Hash] proxy_info (Shodan.proxy)
43
+ # The proxy information.
44
+ #
45
+ # @option proxy_info [String] :host
46
+ # The host of the proxy.
47
+ #
48
+ # @option proxy_info [Integer] :port (COMMON_PROXY_PORT)
49
+ # The port of the proxy.
50
+ #
51
+ # @option proxy_info [String] :user
52
+ # The user name to authenticate as.
53
+ #
54
+ # @option proxy_info [String] :password
55
+ # The password to authenticate with.
56
+ #
57
+ def Shodan.proxy_uri(proxy_info=Shodan.proxy)
58
+ if Shodan.proxy[:host]
59
+ return URI::HTTP.build(
60
+ :host => Shodan.proxy[:host],
61
+ :port => Shodan.proxy[:port],
62
+ :userinfo => "#{Shodan.proxy[:user]}:#{Shodan.proxy[:password]}",
63
+ :path => '/'
64
+ )
65
+ end
66
+ end
67
+
68
+ #
69
+ # The supported Shodan User-Agent Aliases.
70
+ #
71
+ # @return [Hash{String => String}]
72
+ # The User-Agent aliases and strings.
73
+ #
74
+ def Shodan.user_agent_aliases
75
+ WWW::Mechanize::AGENT_ALIASES
76
+ end
77
+
78
+ #
79
+ # The default Shodan User-Agent
80
+ #
81
+ # @return [String]
82
+ # The default User-Agent string used by Shodan.
83
+ #
84
+ def Shodan.user_agent
85
+ @@shodan_user_agent ||= Shodan.user_agent_aliases['Windows IE 6']
86
+ end
87
+
88
+ #
89
+ # Sets the default Shodan User-Agent.
90
+ #
91
+ # @param [String] agent
92
+ # The User-Agent string to use.
93
+ #
94
+ # @return [String]
95
+ # The new User-Agent string.
96
+ #
97
+ def Shodan.user_agent=(agent)
98
+ @@shodan_user_agent = agent
99
+ end
100
+
101
+ #
102
+ # Sets the default Shodan User-Agent alias.
103
+ #
104
+ # @param [String] name
105
+ # The alias of the User-Agent string to use.
106
+ #
107
+ # @return [String]
108
+ # The new User-Agent string.
109
+ #
110
+ def Shodan.user_agent_alias=(name)
111
+ @@shodan_user_agent = Shodan.user_agent_aliases[name.to_s]
112
+ end
113
+
114
+ #
115
+ # Creates a new WWW::Mechanize agent.
116
+ #
117
+ # @param [Hash] options
118
+ # Additional options.
119
+ #
120
+ # @option options [String] :user_agent_alias
121
+ # The User-Agent alias to use.
122
+ #
123
+ # @option options [String] :user_agent (Shodan.user_agent)
124
+ # The User-Agent string to use.
125
+ #
126
+ # @option options [Hash] :proxy (Shodan.proxy)
127
+ # The proxy information to use.
128
+ #
129
+ # @option :proxy [String] :host
130
+ # The host of the proxy.
131
+ #
132
+ # @option :proxy [Integer] :port
133
+ # The port of the proxy.
134
+ #
135
+ # @option :proxy [String] :user
136
+ # The user to authenticate as.
137
+ #
138
+ # @option :proxy [String] :password
139
+ # The password to authenticate with.
140
+ #
141
+ # @example Creating a new Mechanize agent.
142
+ # Shodan.web_agent
143
+ #
144
+ # @example Creating a new Mechanize agent with a User-Agent alias.
145
+ # Shodan.web_agent(:user_agent_alias => 'Linux Mozilla')
146
+ #
147
+ # @example Creating a new Mechanize agent with a User-Agent string.
148
+ # Shodan.web_agent(:user_agent => 'Google Bot')
149
+ #
150
+ def Shodan.web_agent(options={},&block)
151
+ agent = WWW::Mechanize.new
152
+
153
+ if options[:user_agent_alias]
154
+ agent.user_agent_alias = options[:user_agent_alias]
155
+ elsif options[:user_agent]
156
+ agent.user_agent = options[:user_agent]
157
+ elsif Shodan.user_agent
158
+ agent.user_agent = Shodan.user_agent
159
+ end
160
+
161
+ proxy = (options[:proxy] || Shodan.proxy)
162
+ if proxy[:host]
163
+ agent.set_proxy(proxy[:host],proxy[:port],proxy[:user],proxy[:password])
164
+ end
165
+
166
+ block.call(agent) if block
167
+ return agent
168
+ end
169
+
170
+ #
171
+ # Creates a new Query object.
172
+ #
173
+ # @param [Hash] options
174
+ # Additional options.
175
+ #
176
+ # @option options [String] :query
177
+ # The query expression.
178
+ #
179
+ # @option options [Array<String>] :countries
180
+ # The Country Codes to search within.
181
+ #
182
+ # @option options [String] :country
183
+ # A Country Code to search within.
184
+ #
185
+ # @option options [Array<String>] :hostnames
186
+ # The host names to search for.
187
+ #
188
+ # @option options [String] :hostname
189
+ # A host name to search for.
190
+ #
191
+ # @option options [Array<String>] :networks
192
+ # The CIDR network blocks to search within.
193
+ #
194
+ # @option options [String] :network
195
+ # A CIDR network blocks to search within.
196
+ #
197
+ # @option options [Array<Integer>] :ports
198
+ # The ports to search for.
199
+ #
200
+ # @option options [Integer] :port
201
+ # A port to search for.
202
+ #
203
+ # @yield [query]
204
+ # If a block is given, it will be passed the newly created Query
205
+ # object.
206
+ #
207
+ # @yieldparam [Query] query
208
+ # The newly created Query object.
209
+ #
210
+ # @return [Query]
211
+ # The new Query object.
212
+ #
213
+ def Shodan.query(options={},&block)
214
+ Query.new(options,&block)
215
+ end
216
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ module Shodan
22
+ # shodanrb version
23
+ VERSION = '0.1.0'
24
+ end
data/lib/shodan.rb ADDED
@@ -0,0 +1,23 @@
1
+ #
2
+ # shodan-ruby - A Ruby interface to SHODAN, a computer search engine.
3
+ #
4
+ # Copyright (c) 2009 Hal Brodigan (postmodern.mod3 at gmail.com)
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19
+ #
20
+
21
+ require 'shodan/query'
22
+ require 'shodan/shodan'
23
+ require 'shodan/version'
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for "has Pages" do
4
+ it "should have a first page" do
5
+ @query.first_page.should_not be_nil
6
+ end
7
+
8
+ it "should allow indexed access" do
9
+ @query[1].should_not be_nil
10
+ end
11
+
12
+ it "should allow accessing multiple pages" do
13
+ pages = @query.pages(1..2)
14
+ pages.should_not be_nil
15
+ pages.length.should == 2
16
+ end
17
+ end
data/spec/host_spec.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'shodan/host'
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Host do
6
+ describe "non-HTTP host" do
7
+ before(:all) do
8
+ @host = Host.new('127.0.0.1','16.11.2009','SSH-2.0-OpenSSH_4.2')
9
+ end
10
+
11
+ it "should parse the date" do
12
+ @host.date.class.should == Date
13
+ end
14
+
15
+ it "should not have a HTTP version" do
16
+ @host.http_version.should be_nil
17
+ end
18
+
19
+ it "should not have a HTTP status code" do
20
+ @host.http_code.should be_nil
21
+ end
22
+
23
+ it "should not have a HTTP status message" do
24
+ @host.http_status.should be_nil
25
+ end
26
+
27
+ it "should not have HTTP response headers" do
28
+ @host.http_headers.should == {}
29
+ end
30
+
31
+ it "should not have a HTTP response body" do
32
+ @host.http_body.should be_nil
33
+ end
34
+ end
35
+
36
+ describe "HTTP host" do
37
+ before(:all) do
38
+ @host = Host.new(
39
+ '127.0.0.1',
40
+ '16.11.2009',
41
+ %{
42
+ HTTP/1.0 200 OK
43
+ X-powered-by: PHP/5.3.0
44
+ Transfer-encoding: chunked
45
+ X-gpg-key: http://chrislea.com/gpgkey.txt http://chrislea.com/gpgkey-virb.txt
46
+ Vary: Cookie
47
+ X-ssh-key: http://chrislea.com/sshkey.txt
48
+ Server: nginx/0.8.24
49
+ Connection: keep-alive
50
+ Date: Fri, 20 Nov 2009 00:52:24 GMT
51
+ X-the-question: Quis custodiet ipsos custodes?
52
+ Content-type: text/html; charset=UTF-8
53
+ X-pingback: http://chrislea.com/xmlrpc.php
54
+
55
+ <HTML>
56
+ </HTML>
57
+ }.strip
58
+ )
59
+ end
60
+
61
+ it "should parse the date" do
62
+ @host.date.class.should == Date
63
+ end
64
+
65
+ it "should not have a HTTP version" do
66
+ @host.http_version.should == '1.0'
67
+ end
68
+
69
+ it "should not have a HTTP status code" do
70
+ @host.http_code.should == 200
71
+ end
72
+
73
+ it "should not have a HTTP status message" do
74
+ @host.http_status.should == 'OK'
75
+ end
76
+
77
+ it "should not have HTTP response headers" do
78
+ @host.http_headers.should == {
79
+ 'X-powered-by' => 'PHP/5.3.0',
80
+ 'Transfer-encoding' => 'chunked',
81
+ 'X-gpg-key' => 'http://chrislea.com/gpgkey.txt http://chrislea.com/gpgkey-virb.txt',
82
+ 'Vary' => 'Cookie',
83
+ 'X-ssh-key' => 'http://chrislea.com/sshkey.txt',
84
+ 'Server' => 'nginx/0.8.24',
85
+ 'Connection' => 'keep-alive',
86
+ 'Date' => 'Fri, 20 Nov 2009 00:52:24 GMT',
87
+ 'X-the-question' => 'Quis custodiet ipsos custodes?',
88
+ 'Content-type' => 'text/html; charset=UTF-8',
89
+ 'X-pingback' => 'http://chrislea.com/xmlrpc.php'
90
+ }
91
+ end
92
+
93
+ it "should provide a server name" do
94
+ @host.server_name.should == 'nginx'
95
+ end
96
+
97
+ it "should provide a server version" do
98
+ @host.server_version.should == '0.8.24'
99
+ end
100
+
101
+ it "should provide transparent access to the HTTP response headers" do
102
+ @host.x_the_question.should == 'Quis custodiet ipsos custodes?'
103
+ end
104
+
105
+ it "should raise a NoMethodError when accessing missing headers" do
106
+ lambda {
107
+ @host.lol
108
+ }.should raise_error(NoMethodError)
109
+ end
110
+
111
+ it "should not have a HTTP response body" do
112
+ @host.http_body.should == "<HTML>\n</HTML>"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,31 @@
1
+ require 'shodan/page'
2
+
3
+ require 'spec_helper'
4
+
5
+ shared_examples_for "Page has Hosts" do
6
+ it "should have hosts" do
7
+ @page.length.should_not == 0
8
+ end
9
+
10
+ it "should have the maximum amount of hosts per page" do
11
+ @page.length.should == @query.results_per_page
12
+ end
13
+
14
+ it "should have IP addresses" do
15
+ @page.ips.should_not be_empty
16
+ end
17
+
18
+ it "should have valid IP addresses" do
19
+ @page.each_ip do |ip|
20
+ ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
21
+ end
22
+ end
23
+
24
+ it "should have dates" do
25
+ @page.dates.should_not be_empty
26
+ end
27
+
28
+ it "should have responses" do
29
+ @page.responses.should_not be_empty
30
+ end
31
+ end