shodanz 1.0.6 → 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utils.rb'
4
+
5
+ # fronzen_string_literal: true
6
+
7
+ module Shodanz
8
+ module API
9
+ # Utils provides simply get, post, and slurp_stream functionality
10
+ # to the client. Under the hood they support both async and non-async
11
+ # usage. You should basically never need to use these methods directly.
12
+ #
13
+ # @author Kent 'picat' Gruber
14
+ module Utils
15
+ # Perform a direct GET HTTP request to the REST API.
16
+ def get(path, **params)
17
+ return sync_get(path, **params) unless Async::Task.current?
18
+
19
+ async_get(path, **params)
20
+ end
21
+
22
+ # Perform a direct POST HTTP request to the REST API.
23
+ def post(path, body: nil, **params)
24
+ return sync_post(path, params: params, body: body) unless Async::Task.current?
25
+
26
+ async_post(path, params: params, body: body)
27
+ end
28
+
29
+ # Perform the main function of consuming the streaming API.
30
+ def slurp_stream(path, **params)
31
+ if Async::Task.current?
32
+ async_slurp_stream(path, **params) do |result|
33
+ yield result
34
+ end
35
+ else
36
+ sync_slurp_stream(path, **params) do |result|
37
+ yield result
38
+ end
39
+ end
40
+ end
41
+
42
+ def turn_into_query(**params)
43
+ filters = params.reject { |key, _| key == :query }
44
+ filters.each do |key, value|
45
+ params[:query] << " #{key}:#{value}"
46
+ end
47
+ params.select { |key, _| key == :query }
48
+ end
49
+
50
+ def turn_into_facets(**facets)
51
+ return {} if facets.nil?
52
+
53
+ filters = facets.reject { |key, _| key == :facets }
54
+ facets[:facets] = []
55
+ filters.each do |key, value|
56
+ facets[:facets] << "#{key}:#{value}"
57
+ end
58
+ facets[:facets] = facets[:facets].join(',')
59
+ facets.select { |key, _| key == :facets }
60
+ end
61
+
62
+ private
63
+
64
+ RATELIMIT = 'rate limit reached'
65
+ NOINFO = 'no information available'
66
+ NOQUERY = 'empty search query'
67
+ ACCESSDENIED = 'access denied'
68
+ INVALIDKEY = 'invalid API key'
69
+
70
+ def handle_any_json_errors(json)
71
+ if json.is_a?(Hash) && json.key?('error')
72
+ raise Shodanz::Errors::RateLimited if json['error'].casecmp(RATELIMIT) >= 0
73
+ raise Shodanz::Errors::NoInformation if json['error'].casecmp(NOINFO) >= 0
74
+ raise Shodanz::Errors::NoQuery if json['error'].casecmp(NOQUERY) >= 0
75
+ raise Shodanz::Errors::AccessDenied if json['error'].casecmp(ACCESSDENIED) >= 0
76
+ raise Shodanz::Errors::InvalidKey if json['error'].casecmp(INVALIDKEY) >= 0
77
+ end
78
+ return json
79
+ end
80
+
81
+ def getter(path, **params)
82
+ # param keys should all be strings
83
+ params = params.transform_keys(&:to_s)
84
+ # build up url string based on special params
85
+ url = "/#{path}?key=#{@key}"
86
+ # special params
87
+ params.each do |param,value|
88
+ next if value.is_a?(String) && value.empty?
89
+ value = URI.encode_www_form_component("#{value}")
90
+ url += "&#{param}=#{value}"
91
+ end
92
+
93
+ resp = @client.get(url)
94
+
95
+ if resp.success?
96
+ # parse all lines in the response body as JSON
97
+ json = JSON.parse(resp.body.join)
98
+
99
+ handle_any_json_errors(json)
100
+
101
+ return json
102
+ else
103
+ raise "Got response status #{resp.status}"
104
+ end
105
+ ensure
106
+ @client.pool.close
107
+ resp&.close
108
+ end
109
+
110
+ def poster(path, one_shot: false, params: nil, body: nil)
111
+ # param keys should all be strings
112
+ params = params.transform_keys(&:to_s)
113
+ # and the key param is constant
114
+ params["key"] = @key
115
+ # encode as a URL string
116
+ params = URI.encode_www_form(params)
117
+ # optional JSON body string
118
+ json_body = body.nil? ? nil : JSON.dump(body)
119
+ # build URL path
120
+ path = "/#{path}?#{params}"
121
+
122
+ # make POST request to server
123
+ resp = @client.post(path, nil, json_body)
124
+
125
+ if resp.success?
126
+ json = JSON.parse(resp.body.join)
127
+
128
+ handle_any_json_errors(json)
129
+
130
+ return json
131
+ else
132
+ raise "Got response status #{resp.status}"
133
+ end
134
+ ensure
135
+ @client.pool.close
136
+ resp&.close
137
+ end
138
+
139
+ def slurper(path, **params)
140
+ # param keys should all be strings
141
+ params = params.transform_keys(&:to_s)
142
+ # check if limit
143
+ if (limit = params.delete('limit'))
144
+ counter = 0
145
+ end
146
+ # make GET request to server
147
+ resp = @client.get("/#{path}?key=#{@key}", params)
148
+ # read body line-by-line
149
+ until resp.body.nil? || resp.body.empty?
150
+ resp.body.read.each_line do |line|
151
+ next if line.strip.empty?
152
+
153
+ yield JSON.parse(line)
154
+ if limit
155
+ counter += 1
156
+ resp.close if counter == limit
157
+ end
158
+ end
159
+ end
160
+ ensure
161
+ resp&.close
162
+ end
163
+
164
+ def async_get(path, **params)
165
+ Async::Task.current.async do
166
+ getter(path, **params)
167
+ end
168
+ end
169
+
170
+ def sync_get(path, **params)
171
+ Async do
172
+ getter(path, **params)
173
+ end.wait
174
+ end
175
+
176
+ def async_post(path, params: nil, body: nil)
177
+ Async::Task.current.async do
178
+ poster(path, params: params, body: body)
179
+ end
180
+ end
181
+
182
+ def sync_post(path, params: nil, body: nil)
183
+ Async do
184
+ poster(path, params: params, body: body)
185
+ end.wait
186
+ end
187
+
188
+ def async_slurp_stream(path, **params)
189
+ Async::Task.current.async do
190
+ slurper(path, **params) { |data| yield data }
191
+ end
192
+ end
193
+
194
+ def sync_slurp_stream(path, **params)
195
+ Async do
196
+ slurper(path, **params) { |data| yield data }
197
+ end.wait
198
+ end
199
+ end
200
+ end
201
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shodanz
2
4
  # General client container class for all three
3
5
  # of the available API endpoints in a
@@ -17,7 +19,8 @@ module Shodanz
17
19
  # Optionally provide your Shodan API key, or the environment
18
20
  # variable SHODAN_API_KEY will be used.
19
21
  def initialize(key: ENV['SHODAN_API_KEY'])
20
- raise "No API key has been found or provided! ( setup your SHODAN_API_KEY environment varialbe )" if key.nil?
22
+ raise Shodanz::Errors::NoAPIKey if key.nil?
23
+
21
24
  # pass the given API key to each of the underlying clients
22
25
  #
23
26
  # Note: you can optionally change these API keys later, if you
@@ -27,5 +30,89 @@ module Shodanz
27
30
  @streaming_api = Shodanz.api.streaming.new(key: key)
28
31
  @exploits_api = Shodanz.api.exploits.new(key: key)
29
32
  end
33
+
34
+ def host(ip, **params)
35
+ rest_api.host(ip, **params)
36
+ end
37
+
38
+ def host_count(query = '', facets: {}, **params)
39
+ rest_api.host_count(query, facets: facets, **params)
40
+ end
41
+
42
+ def host_search(query = '', facets: {}, page: 1, minify: true, **params)
43
+ rest_api.host_search(query, facets: facets, page: page, minify: minify, **params)
44
+ end
45
+
46
+ def host_search_tokens(query = '', **params)
47
+ rest_api.host_search(query, params)
48
+ end
49
+
50
+ def ports
51
+ rest_api.ports
52
+ end
53
+
54
+ def protocols
55
+ rest_api.protocols
56
+ end
57
+
58
+ def scan(*ips)
59
+ rest_api.scan(ips)
60
+ end
61
+
62
+ def crawl_for(**params)
63
+ rest_api.scan(params)
64
+ end
65
+
66
+ def scan_status(id)
67
+ rest_api.scan_status(id)
68
+ end
69
+
70
+ def community_queries(**params)
71
+ rest_api.community_queries(params)
72
+ end
73
+
74
+ def search_for_community_query(query, **params)
75
+ rest_api.search_for_community_query(query, params)
76
+ end
77
+
78
+ def popular_query_tags(size = 10)
79
+ rest_api.popular_query_tags(size)
80
+ end
81
+
82
+ def profile
83
+ rest_api.profile
84
+ end
85
+
86
+ def resolve(*hostnames)
87
+ rest_api.resolve(hostnames)
88
+ end
89
+
90
+ def reverse_lookup(*ips)
91
+ rest_api.reverse_lookup(ips)
92
+ end
93
+
94
+ def http_headers
95
+ rest_api.http_headers
96
+ end
97
+
98
+ def my_ip
99
+ rest_api.my_ip
100
+ end
101
+
102
+ def honeypot_score(ip)
103
+ rest_api.honeypot_score(ip)
104
+ end
105
+
106
+ def info
107
+ rest_api.info
108
+ end
109
+
110
+ def exploit_search(query = '', page: 1, **params)
111
+ exploits_api.search(query, page: page, **params)
112
+ end
113
+
114
+ def exploit_count(query = '', page: 1, **params)
115
+ exploits_api.count(query, page: page, **params)
116
+ end
30
117
  end
31
118
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shodanz
4
+ module Errors
5
+ class RateLimited < StandardError
6
+ def initialize(msg = 'Request rate limit reached (1 request/ second). Please wait a second before trying again and slow down your API calls.')
7
+ super
8
+ end
9
+ end
10
+
11
+ class NoInformation < StandardError
12
+ def initialize(msg = 'No information available.')
13
+ super
14
+ end
15
+ end
16
+
17
+ class NoAPIKey < StandardError
18
+ def initialize(msg = 'No API key has been found or provided! ( setup your SHODAN_API_KEY environment variable )')
19
+ super
20
+ end
21
+ end
22
+
23
+ class NoQuery < StandardError
24
+ def initialize(msg = 'Empty search query.')
25
+ super
26
+ end
27
+ end
28
+
29
+ class AccessDenied < StandardError
30
+ def initialize(msg = 'Shodan subscription doesn\'t support action, check API permissions!')
31
+ super
32
+ end
33
+ end
34
+
35
+ class InvalidKey < StandardError
36
+ def initialize(msg = 'Invalid API key used, or none given!')
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Shodanz
2
- VERSION = '1.0.6'.freeze
4
+ VERSION = '2.0.4'
3
5
  end
@@ -1,29 +1,32 @@
1
- # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "shodanz/version"
5
+ require 'shodanz/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "shodanz"
8
+ spec.name = 'shodanz'
8
9
  spec.version = Shodanz::VERSION
9
10
  spec.authors = ["Kent 'picatz' Gruber"]
10
- spec.email = ["kgruber1@emich.edu"]
11
+ spec.email = ['kgruber1@emich.edu']
11
12
 
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"
13
+ spec.summary = "A modern, async Ruby gem for Shodan, the world's first search engine for Internet-connected devices."
14
+ spec.description = 'Featuring full support for the REST, Streaming and Exploits API'
15
+ spec.homepage = 'https://github.com/picatz/shodanz'
16
+ spec.license = 'MIT'
16
17
 
17
18
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
19
  f.match(%r{^(test|spec|features)/})
19
20
  end
20
- spec.require_paths = ["lib"]
21
-
22
- spec.add_dependency "unirest", "1.1.2"
23
- spec.add_dependency "json" # specifying version breaks things, what?
24
- spec.add_dependency "oj", "3.5.0"
25
-
26
- spec.add_development_dependency "bundler", "~> 1.16.1"
27
- spec.add_development_dependency "rake", "~> 12.3.1"
28
- spec.add_development_dependency "rspec", "~> 3.7.0"
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_dependency 'async-http', '>= 0.38.1', '< 0.53.0'
24
+ spec.add_dependency 'async', '>= 1.17.1', '< 1.28.0'
25
+
26
+ spec.add_development_dependency 'async-rspec', '~> 1.14.0'
27
+ spec.add_development_dependency 'bundler', '~> 2.1.2'
28
+ spec.add_development_dependency 'pry', '~> 0.13.0'
29
+ spec.add_development_dependency 'rake', '~> 13.0.0'
30
+ spec.add_development_dependency 'rb-readline', '~> 0.5.5'
31
+ spec.add_development_dependency 'rspec', '~> 3.10.0'
29
32
  end
metadata CHANGED
@@ -1,99 +1,139 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shodanz
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 2.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kent 'picatz' Gruber
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-04 00:00:00.000000000 Z
11
+ date: 2020-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: unirest
14
+ name: async-http
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.38.1
20
+ - - "<"
18
21
  - !ruby/object:Gem::Version
19
- version: 1.1.2
22
+ version: 0.53.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - '='
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.38.1
30
+ - - "<"
25
31
  - !ruby/object:Gem::Version
26
- version: 1.1.2
32
+ version: 0.53.0
27
33
  - !ruby/object:Gem::Dependency
28
- name: json
34
+ name: async
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
- version: '0'
39
+ version: 1.17.1
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: 1.28.0
34
43
  type: :runtime
35
44
  prerelease: false
36
45
  version_requirements: !ruby/object:Gem::Requirement
37
46
  requirements:
38
47
  - - ">="
39
48
  - !ruby/object:Gem::Version
40
- version: '0'
49
+ version: 1.17.1
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 1.28.0
41
53
  - !ruby/object:Gem::Dependency
42
- name: oj
54
+ name: async-rspec
43
55
  requirement: !ruby/object:Gem::Requirement
44
56
  requirements:
45
- - - '='
57
+ - - "~>"
46
58
  - !ruby/object:Gem::Version
47
- version: 3.5.0
48
- type: :runtime
59
+ version: 1.14.0
60
+ type: :development
49
61
  prerelease: false
50
62
  version_requirements: !ruby/object:Gem::Requirement
51
63
  requirements:
52
- - - '='
64
+ - - "~>"
53
65
  - !ruby/object:Gem::Version
54
- version: 3.5.0
66
+ version: 1.14.0
55
67
  - !ruby/object:Gem::Dependency
56
68
  name: bundler
57
69
  requirement: !ruby/object:Gem::Requirement
58
70
  requirements:
59
71
  - - "~>"
60
72
  - !ruby/object:Gem::Version
61
- version: 1.16.1
73
+ version: 2.1.2
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 2.1.2
81
+ - !ruby/object:Gem::Dependency
82
+ name: pry
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 0.13.0
62
88
  type: :development
63
89
  prerelease: false
64
90
  version_requirements: !ruby/object:Gem::Requirement
65
91
  requirements:
66
92
  - - "~>"
67
93
  - !ruby/object:Gem::Version
68
- version: 1.16.1
94
+ version: 0.13.0
69
95
  - !ruby/object:Gem::Dependency
70
96
  name: rake
71
97
  requirement: !ruby/object:Gem::Requirement
72
98
  requirements:
73
99
  - - "~>"
74
100
  - !ruby/object:Gem::Version
75
- version: 12.3.1
101
+ version: 13.0.0
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: 13.0.0
109
+ - !ruby/object:Gem::Dependency
110
+ name: rb-readline
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: 0.5.5
76
116
  type: :development
77
117
  prerelease: false
78
118
  version_requirements: !ruby/object:Gem::Requirement
79
119
  requirements:
80
120
  - - "~>"
81
121
  - !ruby/object:Gem::Version
82
- version: 12.3.1
122
+ version: 0.5.5
83
123
  - !ruby/object:Gem::Dependency
84
124
  name: rspec
85
125
  requirement: !ruby/object:Gem::Requirement
86
126
  requirements:
87
127
  - - "~>"
88
128
  - !ruby/object:Gem::Version
89
- version: 3.7.0
129
+ version: 3.10.0
90
130
  type: :development
91
131
  prerelease: false
92
132
  version_requirements: !ruby/object:Gem::Requirement
93
133
  requirements:
94
134
  - - "~>"
95
135
  - !ruby/object:Gem::Version
96
- version: 3.7.0
136
+ version: 3.10.0
97
137
  description: Featuring full support for the REST, Streaming and Exploits API
98
138
  email:
99
139
  - kgruber1@emich.edu
@@ -101,6 +141,7 @@ executables: []
101
141
  extensions: []
102
142
  extra_rdoc_files: []
103
143
  files:
144
+ - ".github/workflows/ci.yml"
104
145
  - ".gitignore"
105
146
  - ".rspec"
106
147
  - ".travis.yml"
@@ -109,6 +150,9 @@ files:
109
150
  - LICENSE.txt
110
151
  - README.md
111
152
  - Rakefile
153
+ - examples/async_honeypot_detector.rb
154
+ - examples/async_host_search_example.rb
155
+ - examples/async_stream_example.rb
112
156
  - examples/debug.rb
113
157
  - examples/streaming_banner_product_stats.rb
114
158
  - examples/top_10_countries_running.rb
@@ -118,7 +162,9 @@ files:
118
162
  - lib/shodanz/apis/exploits.rb
119
163
  - lib/shodanz/apis/rest.rb
120
164
  - lib/shodanz/apis/streaming.rb
165
+ - lib/shodanz/apis/utils.rb
121
166
  - lib/shodanz/client.rb
167
+ - lib/shodanz/errors.rb
122
168
  - lib/shodanz/version.rb
123
169
  - shodanz.gemspec
124
170
  homepage: https://github.com/picatz/shodanz
@@ -140,10 +186,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
186
  - !ruby/object:Gem::Version
141
187
  version: '0'
142
188
  requirements: []
143
- rubyforge_project:
144
- rubygems_version: 2.7.6
189
+ rubygems_version: 3.0.6
145
190
  signing_key:
146
191
  specification_version: 4
147
- summary: A modern Ruby gem for Shodan, the world's first search engine for Internet-connected
148
- devices.
192
+ summary: A modern, async Ruby gem for Shodan, the world's first search engine for
193
+ Internet-connected devices.
149
194
  test_files: []